Skip to content

Commit

Permalink
Add support for custom name options
Browse files Browse the repository at this point in the history
Where previously assumed default values for the beancount
'name_*' options, now supports customised values.

Implementation now sets and gets these values via methods
on the config module. The `utils.set_account_root_names`
method has also been added to set these options from
inspection of a beancount file.

Adds optional --main argument to the 'make' cli subcommand
to take a ledger from which to read name options. The only
other subcommand that requries knowledge of names is 'addrx'
and this already required the main ledger to be passed.

Updates README to reflect new implementation.
  • Loading branch information
maread99 committed Jun 28, 2024
1 parent 0a0e555 commit 02be581
Show file tree
Hide file tree
Showing 15 changed files with 1,112 additions and 22 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ $ beanahead make x -f x
```
include "x.beancount"
```

So, if you want to include both regular and ad hoc expected transactions then you should have created three new `.beancount` files and added two 'include' lines to top of your main ledger.

> :information_source: The -f option provides for defining the filename (`make` will add the `.beancount` extension). If -f is not passed then default names will be used which are as those explicitly passed in the examples.
Expand All @@ -111,12 +110,15 @@ The [examples/new_empty_files](./examples/new_empty_files) folder includes a sam
- [Regular Expected Transactions Ledger](./examples/new_empty_files/rx.beancount)
- [Expected Transactions Ledger](./examples/new_empty_files/x.beancount)

## Regular Expected Transactions
NB If you're not using the default account root names (e.g. 'Assets', 'Income', 'Expenses' etc) then you'll also need to use the optional --main argument to provide the path to a ledger from which beanahead can read the customised name options. Example:
```
$ beanahead make x -f x --main my_ledger
```

## Regular Expected Transactions
Regular expected transactions are defined on the Regular Expected Transaction _Definitions_ file. The `addrx` command can then be used to populate the Regular Expected Transactions _Ledger_ with transactions generated from these definitions.

### Defining regular transactions

A new regular transaction can be defined at any time by adding a single transaction to the definitions file (the 'initial definition'). The date of this transaction will serve as the anchor off which future transactions are evaluated.

The following initial definition would generate regular transactions on the 5th of every month, with the first generated transaction dated 2022-10-05.
Expand Down Expand Up @@ -376,7 +378,8 @@ With a bit of luck and perhaps a tweak or two to your ledger, your `bean-check`
## Worth remembering
> :warning: Whenever an expected transactions ledger or the regular expected transaction definition files are updated the entries are resorted and the file is overwritten - anything that is not a directive (e.g. comments) will be lost.
## Developers
## Options
### Print to stderr
If you are employing a workflow that directs output through `sys.stdout` then this will merge with Beanahead's own output to this stream. To avoid such conflicts `beanahead` provides for directing its output to `sys.stderr`. This can be set from the command line by preceeding any subcommand with --print_stderr, for example...
```
$ beanahead --print_stderr exp rx x
Expand All @@ -389,6 +392,15 @@ If using the underlying functions directly in the codebase then the print stream
- `set_print_stderr()`
- `set_print_stdout()`

### Custom account root names
If you're not using the default account root names (e.g. 'Assets', 'Income', 'Expenses' etc) then beanahead will need to know the values that you are using.

The subcommands of the cli that require these names get them from a ledger ('addrx' will read them from the main ledger which is a required argument, 'make' will read them from any ledger passed to its optional --main argument).

If using the underlying functions directly in the codebase then the names can be set either by passing a dictionary to `config.set_account_root_names` or by passing a ledger that contains these options to `utils.set_account_root_names`.

The currently set names can be inspected via `config.get_account_root_names()` and reset to the default values with`config.reset_account_root_names()`.

## Alternative packages
The beancount community offers a considerable array of add-on packages, many of which are well-rated and maintained. Below I've noted those I know of with functionality that includes some of what `beanahead` offers. Which package you're likely to find most useful will come down to your specific circumstances and requirements - horses for courses.
* [beancount-import](https://github.com/jbms/beancount-import) - an importer interface. Functionality provides for adding expected transactions directly to the main ledger and later merging these with imported transactions via a web-based UI. It requires implementing the importer interface and doesn't directly provide for regular expected transactions. But, if that import interface works for you then you'll probably want to be using `beancount-import`. (If you need the regular trasactions functionality provided by `beanahead`, just use `beanahead` to generate the transactions, copy them over to your ledger and let `beancount-import` handle the subsequent reconcilation.)
Expand Down
90 changes: 87 additions & 3 deletions src/beanahead/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
_print_stdout = True


def get_print_file():
"""Get stream to print to."""
return sys.stdout if _print_stdout else sys.stderr


def set_print_stdout():
"""Set output stream for print to stdout."""
global _print_stdout
Expand All @@ -17,6 +22,85 @@ def set_print_stderr():
_print_stdout = False


def get_print_file():
"""Get stream to print to."""
return sys.stdout if _print_stdout else sys.stderr
DEFAULT_ACCOUNT_ROOT_NAMES = {
"name_assets": "Assets",
"name_liabilities": "Liabilities",
"name_equity": "Equity",
"name_income": "Income",
"name_expenses": "Expenses",
}

_account_root_names = DEFAULT_ACCOUNT_ROOT_NAMES.copy()


def get_account_root_names() -> dict[str, str]:
"""Get account root names."""
return _account_root_names.copy()


def set_account_root_names(names: dict) -> dict[str, str]:
"""Set account root names.
Use this method to set the account root names.
Default account root names that otherwise prevail are:
{
'name_assets': 'Assets',
'name_liabilities': 'Liabilities',
'name_equity': 'Equity',
'name_income': 'Income',
'name_expenses': 'Expenses',
}
Parameters
----------
names
Dictionary with:
keys: str
Any of the account root name options {'name_assets',
'name_expenses', 'name_income', 'name_liabilities',
'name_equity'}
values: str
Corresponding account root name.
Returns
-------
account_root_names: dict[str, str]
Newly set account root names.
"""
global _account_root_names
diff = set(names) - set(_account_root_names)
if diff:
raise ValueError(
f"'names' parameter can only contain keys: {set(_account_root_names)},"
f" although received 'names' included keys: {diff}"
)
_account_root_names |= names
set_names = get_account_root_names()
assert _account_root_names == set_names
return set_names


def reset_account_root_names() -> dict[str, str]:
"""Set account root names to default values.
Default account root names are:
{
'name_assets': 'Assets',
'name_liabilities': 'Liabilities',
'name_equity': 'Equity',
'name_income': 'Income',
'name_expenses': 'Expenses',
}
Returns
-------
account_root_names: dict[str, str]
Newly set account root names.
"""
global _account_root_names
_account_root_names |= DEFAULT_ACCOUNT_ROOT_NAMES
set_names = get_account_root_names()
assert _account_root_names == set_names
return set_names
6 changes: 3 additions & 3 deletions src/beanahead/rx_txns.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from beancount.parser import parser
from beancount.parser.printer import EntryPrinter

from . import utils, errors
from . import utils, errors, config
from .errors import BeanaheadWriteError, BeancountLoaderErrors

END_DFLT = utils.TODAY + datetime.timedelta(weeks=13)
Expand Down Expand Up @@ -209,9 +209,9 @@ def get_definition_group(definition: Transaction) -> GrouperKey:
if account == bal_sheet_account:
continue
account_type = get_account_type(account)
if account_type == "Assets":
if account_type == config.get_account_root_names()["name_assets"]:
other_sides.add("Assets")
elif account_type == "Income":
elif account_type == config.get_account_root_names()["name_income"]:
other_sides.add("Income")
else:
other_sides.add("Expenses")
Expand Down
24 changes: 21 additions & 3 deletions src/beanahead/scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,19 @@ def main():
parser_make.add_argument(
*["-f", "--filename"],
help=(
"Name of new beanahead file. By default, as `key`.\nShould not"
" include the .beancount extension."
"Name of new beanahead file. By default, as `key`."
"\nShould not include the .beancount extension."
),
metavar="",
)

parser_make.add_argument(
*["-m", "--main"],
help=(
"Path to existing main Ledger file. Only required if\n"
"account root names are not the default values, in\n"
"which case will use root names as defined by\n"
"the options on this ledger.\n"
),
metavar="",
)
Expand Down Expand Up @@ -270,11 +281,18 @@ def main():
# Call pass-through function corresponding with subcommand
args = parser.parse_args()

# Set print stream and revert to stdout
# Set print stream
if args.print_stderr:
config.set_print_stderr()
# Set root account names
if "main" in args and args.main is not None:
utils.set_account_root_names(args.main)

args.func(args)

# Revert options to default values
config.set_print_stdout()
config.reset_account_root_names()


if __name__ == "__main__":
Expand Down
47 changes: 42 additions & 5 deletions src/beanahead/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from beancount.parser import parser, printer

from . import config
from .config import get_account_root_names, DEFAULT_ACCOUNT_ROOT_NAMES
from .errors import (
BeancountFileExistsError,
BeanaheadFileExistsError,
Expand Down Expand Up @@ -132,6 +133,12 @@ def compose_header_footer(file_key: str) -> tuple[str, str]:
plugin, tag, comment = config["plugin"], config["tag"], config["comment"]

header = f"""option "title" "{config['title']}"\n"""

for k, v in get_account_root_names().items():
if DEFAULT_ACCOUNT_ROOT_NAMES[k] == v:
continue
header += f'option "{k}" "{v}"\n'

if plugin is not None:
header += f'plugin "{plugin}"\n'
header += f"pushtag #{tag}\n"
Expand Down Expand Up @@ -305,6 +312,32 @@ def get_options(path: Path) -> dict:
return options


def set_account_root_names(filepath: str) -> dict[str, str]:
"""Set account root names from options defined on a beancount file.
Parameters
----------
filepath: str
Path to .beancount file containing options defining account root
names, for example:
option "name_assets" "AltAssetsRootName"
Can be passed as either absolute path or path relative to the cwd.
It is not necessary to include the. beancount extension.
For example, "ledger" would refer to the file 'ledger.beancount' in
the cwd.
Returns
-------
account_root_names: dict[str, str]
Newly set account root names.
"""
path = get_verified_path(filepath)
options = get_options(path)
names = {k: options[k] for k in DEFAULT_ACCOUNT_ROOT_NAMES if k in options}
return config.set_account_root_names(names)


def get_verified_file_key(path: Path) -> str:
"""Verify path to a beanahead file and return its file key.
Expand Down Expand Up @@ -553,10 +586,7 @@ def is_assets_account(string: str) -> bool:
>>> is_assets_account("Assets:US:BofA:Checking")
True
"""
return is_account_type("Assets", string)


BAL_SHEET_ACCS = ["Assets", "Liabilities"]
return is_account_type(get_account_root_names()["name_assets"], string)


def is_balance_sheet_account(string: str) -> bool:
Expand All @@ -582,7 +612,14 @@ def is_balance_sheet_account(string: str) -> bool:
>>> is_balance_sheet_account("Income:US:BayBook:Match401k")
False
"""
return any(is_account_type(acc_type, string) for acc_type in BAL_SHEET_ACCS)
account_root_names = get_account_root_names()
return any(
is_account_type(acc_type, string)
for acc_type in [
account_root_names["name_assets"],
account_root_names["name_liabilities"],
]
)


def get_balance_sheet_accounts(txn: Transaction) -> list[str]:
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,15 @@ def rx_txn_chase(txns_rx) -> abc.Iterator[data.Transaction]:
assert txn.payee == "Chase"
assert txn.date == datetime.date(2022, 10, 31)
yield txn


@pytest.fixture
def account_root_names_dflt() -> abc.Iterator[dict[str, str]]:
"""Expected default account_root_names."""
yield {
"name_assets": "Assets",
"name_liabilities": "Liabilities",
"name_equity": "Equity",
"name_income": "Income",
"name_expenses": "Expenses",
}
Loading

0 comments on commit 02be581

Please sign in to comment.