Interpolate
Four plugins for double-entry accounting system Beancount to interpolate transactions by generating additional entries over time.
They are:
recur
: dublicates all entry postings over timesplit
: dublicates all entry postings over time at fraction of valuedepr
: generates new entries to depreciate target asset/liability posting over given periodspread
: generate new entries to allocate P&L of target income/expense posting over given period
These plugins are triggered by adding metadata or tags to source entries. It's safe to disable at any time. All plugins share the same parser that can set maximal period, custom starting date and minimal step by either number or keyword.
You can use these to define recurring transactions, account for depreciation, smooth transactions over time and make graphs less zig-zag.
This
depr
is not yet compatible with any accounting standards. For tax-compatible yearly depreciation take a look at this plugin by Alok Parlikar under MIT license. All contributions to improvedepr
are welcome.
Install
pip3 install beancount_interpolate --user
Or copy to path used for python. For example, $HOME/.local/lib/python3.7/site-packages/beancount_interpolate/*
would do on Debian. If in doubt, look where beancount
folder is and copy next to it.
Details: Spread
Problem
Let's say John has only two activities
- 10 EUR daily food expenses
- 300 EUR net monthly salary (day 15 in next month)
If we plot John's wealth, it would look like zig-zag and be negative between -150 EUR (right after salary) and -450 EUR (right before), that is big and unstable error for your current wealth. And there could be more incomes/expenses with various 'periods'. In most complicated case John would have multiple income/expense sources with various lenght periods that are unsynchronised, but John wants dashboard with graph of correct daily report on his wealth.
Possible solutions:
- Do nothing and ackowledge the 0-300 EUR error. IMO Is ok only if error is relatively small.
- Do nothing and restrict yourself to do analysis at regular intervals when error is smallest (for example right after salary) and apply known corrections (for example, +150 EUR because it's already middle of month). IMO it is ok only if all periods are synchronised.
- Record 10 EUR income to cash daily. But that violates 'after fact' principle that IMO is more important and IMO that's too much effort and transaction spam.
- Record 10 EUR income to 'work token' assets daily but IMO that's still too much effort and transaction spam.
- Let plugin do No.4 for you. (This repository)
How to use
Enable the plugin (see available options below).
plugin "beancount_interpolate.spread"
Add meta or tags to your transactions. All folllowing transactions does the same.
; Explicit.
2016-06-15 * "The Company" "Simplest salary entry"
Income:TheCompany:NetSalary -310.00 EUR
spreadAfter: "Month @ 2016-05-01"
Assets:MyBank:Checking 310.00 EUR
; Transaction meta applies to all Income/Expense postings if they don't have their own.
2016-06-15 * "The Company" "Simplest salary entry"
spreadAfter: "Month @ 2016-05-01"
Income:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
; Use spreadBefore if that reads better in your case.
2016-06-15 * "The Company" "Simplest salary entry"
spreadBefore: "Month @ 2016-05-31"
Income:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
; Use default period.
2016-06-15 * "The Company" "Simplest salary entry"
spreadAfter: "2016-05-01"
Income:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
; Use date of transaction.
2016-06-15 * "The Company" "Simplest salary entry"
spreadAfter: "Month"
Income:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
; Use date of transaction and default period. Beware the different transaction date.
2016-05-01 * "The Company" "Simplest salary entry" #spreadAfter
Income:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
What happens
First, plugin edits the original transaction like this:
2016-05-01 * "The Company" "Simplest salary entry (Generated by interpolate-spread)" #spread
Liabilities:Current:TheCompany:NetSalary -310.00 EUR
Assets:MyBank:Checking 310.00 EUR
Second, plugin inserts 30 or 31 transactions from 2016-05-01 to 2016-05-31 like this:
2016-05-xx * "The Company" "Simplest salary entry (Generated by interpolate-spread)" #spread
Income:TheCompany:NetSalary -10.00 EUR
Liabilities:Current:TheCompany:NetSalary 10.00 EUR
Details: Recur
Problem
You want to make recurring entry every X days until forever (or some Y days have passed). Recur will duplicate tagged entry for you!
How to use
Enable the plugin (see available options below).
plugin "beancount_interpolate.recur"
Details: Split
Problem
In fact, the argumentation is the same as in spread
, but the only difference is that different usecases needs a bit different treatment. For example, our John want to set aside money for savings daily. To account for it nicely, spread
won't do - we don't need any Liabilities:Current:...
accounts generated for us. A simple duplication will do here.
How to use
Enable the plugin (see available options below).
plugin "beancount_interpolate.split"
Add meta or tags to your transactions. All folllowing transactions does the same.
; Explicit.
2016-01-01 * "Me" "Set aside money for savings"
split: "Year"
Assets:MyBank:Checking -365.00 EUR
Assets:MyBank:Savings 365.00 EUR
; In fact, original date doesn't matter here, as original entry will be deleted.
2016-06-15 * "Me" "Set aside money for savings"
splitAfter: "Year @ 2016-01-01"
Assets:MyBank:Checking -365.00 EUR
Assets:MyBank:Savings 365.00 EUR
; Use can also use tags.
2016-01-01 * "Me" "Set aside money for savings" #split
Assets:MyBank:Checking -365.00 EUR
Assets:MyBank:Savings 365.00 EUR
What happens
First, plugin deletes the original transaction.
Second, plugin inserts transactions every day until today, included.
2016-01-01 * "Me" "Set aside money for savings (Generated by interpolate-split)" #split
Assets:MyBank:Checking -1.00 EUR
Assets:MyBank:Savings 1.00 EUR
2016-01-02 * "Me" "Set aside money for savings (Generated by interpolate-split)" #split
Assets:MyBank:Checking -1.00 EUR
Assets:MyBank:Savings 1.00 EUR
...
Details: Depreciate
Problem
Depreciate technically is the same as spread but from other way around. But practically, you would like to have different settings for your short-term spreads and long-term depreciations.
How to use
Enable the plugin (see available options below).
plugin "beancount_interpolate.depr"
Add meta or tags to your transactions. All folllowing transactions does the same.
; Explicit.
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting"
Assets:Fixed:PC 199.00 EUR
depr: "Year @ 2016-06-15"
Assets:MyBank:Checking -199.00 EUR
; Transaction meta applies to all Assets:Fixed postings if depr is entry-wide.
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting"
depr: "Year @ 2016-06-15"
Assets:Fixed:PC 199.00 EUR
Assets:MyBank:Checking -199.00 EUR
; Use default period (Year).
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting"
depr: "Year"
Assets:Fixed:PC 199.00 EUR
Assets:MyBank:Checking -199.00 EUR
; Use date of transaction.
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting"
depr: "Month"
Assets:Fixed:PC 199.00 EUR
Assets:MyBank:Checking -199.00 EUR
; Use default period (Year) and default date (entry date).
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting"
depr: "empty string or some nonsense"
Assets:Fixed:PC 199.00 EUR
Assets:MyBank:Checking -199.00 EUR
; Use default period (Year) and default date (entry date) using a tag.
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting" #depr
Assets:Fixed:PC 199.00 EUR
Assets:MyBank:Checking -199.00 EUR
What happens
Plugin inserts lots of transactions starting from given date until end (or today) like this:
2016-06-15 * "CornerStore" "Bought new Laptop to do beancounting (depr 1/365)" #depred
Assets:Fixed:PC -00.55 EUR
Expenses:Depreciation:PC 00.55 EUR
2016-06-16 * "CornerStore" "Bought new Laptop to do beancounting (depr 2/365)" #depred
Assets:Fixed:PC -00.54 EUR
Expenses:Depreciation:PC 00.54 EUR
Options
In Beancount, options are passed to plugins as second argument and may be multi-line.
To beancount_interpolate
options have to be formatted as valid JSON.
Options that applies to all four plugins:
aliases_after
- list of strings to look for in meta and tags.default_duration
- integer or string of keyword that plugin will default duration to, if none was provided in mark.default_step
- integer that plugin will default step to, if none was provided in mark.min_value
- decimal that will be the minimal value of leg within created transactions.max_new_tx
- integer that will be the maximal amount of newly created transactions.suffix
- string appended to created transaction annotations. Two variables are n-th transaction and total amount of transactions.tag
- string that as tag will be applied to all created transactions.
Options that applies only to spread
and depr
. For spread
new transactions are created under account name where account_expense
and account_income
prefixes are swapped to account_assets
or account_liab
prefixes respectively. For depr
that other way around: account_assets
and account_liab
prefixes are swapped to account_expense
and account_income
prefixes instead.
account_income
account_expenses
account_assets
account_liab
Defaults
Here are all available options and their default values. Options are passed as serialized object to the plugin.
plugin "beancount_interpolate.recur" "{
'aliases_after': ['recurAfter', 'recur'],
'default_duration': 'Infinite',
'default_step': 'Day',
'default_method': 'SL',
'min_value': 0.05,
'max_new_tx': 9999,
'suffix': ' (recur %d/%d)',
'tag': 'recurred'
}"
plugin "beancount_interpolate.split" "{
'aliases_after': ['splitAfter', 'split'],
'default_duration': 'Month',
'default_step': 'Day',
'default_method': 'SL',
'min_value': 0.05,
'max_new_tx': 9999,
'suffix': ' (split %d/%d)',
'tag': 'splitted'
}"
plugin "beancount_interpolate.spread" "{
'account_income': 'Income',
'account_expenses': 'Expenses',
'account_assets': 'Assets:Current',
'account_liab': 'Liabilities:Current',
'aliases_after': ['spreadAfter', 'spread'],
'default_duration': 'Month',
'default_step': 'Day',
'default_method': 'SL', # Straight Line
'min_value': 0.05, # cannot be smaller than 0.01
'max_new_tx': 9999,
'suffix': ' (spread %d/%d)',
'tag': 'spreaded'
}"
plugin "beancount_interpolate.depr" "{
'account_income': 'Income:Appreciation',
'account_expenses': 'Expenses:Depreciation',
'account_assets': 'Assets:Fixed',
'account_liab': 'Liabilities:Fixed',
'aliases_after': ['deprAfter', 'depr'],
'default_duration': 'Year',
'default_step': 'Day',
'min_value': 0.05, # cannot be smaller than 0.01
'max_new_tx': 9999,
'suffix': ' (depr %d/%d)',
'tag': 'depred'
}"
aliases_after
Details: - Type: list of strings
If any of aliases are found in transaction tag, transaction meta or posting meta, then the plugin will be applied.
default_duration
Details: - Type: integer or string (one of keywords: day, week, month, year, inf, infinite, max)
Default duration to apply when one is not specified explicitly. Note that month and year keywords do not adapt to current period, but are simple constants (PR welcome!)
default_step
Details: - Type: integer
Default step to apply when one is not specified explicitly. In fact, currently it is not possible to specify it explicitly on per-case basis (PR welcome!)
min_value
Details: - Type: decimal
- Restrictions: no less than 0.01
Minimal value of leg when for new created transactions. It will try to do so Here's example how it works:
For example, you want to spread your groceries 10.00 USD over 7 days, but it apparently doesn't divide nicely. So,
- 1st day would get allocated 1.43 USD but -0.001429 is kept aside (10.00/7=1.428571).
- 2nd day would get allocated 1.43 USD but -0.002858 is kept aside (10.00/7=1.428571-0.001429=1.427142)
- 3rd day would get allocated 1.43 USD but -0.004287 is kept aside (10.00/7=1.428571-0.002858=1.425713)
- 4th day would get allocated 1.42 USD but +0.004284 is kept aside (10.00/7=1.428571-0.004287=1.424284)
- 5th day would get allocated 1.43 USD but +0.002855 is kept aside (10.00/7=1.428571+0.004284=1.432855)
- 6th day would get allocated 1.43 USD but +0.001426 is kept aside (10.00/7=1.428571+0.002855=1.431426)
- 7th day would get allocated 1.43 USD and only pure rounding error is kept aside (10.00/7=1.428571+0.001426=1.429997)
The min_value
is required to be at least 0.01 by beancount, but I'd recommend to raise it even further to avoid "1-cent spam" that lowers readability of reports and impacts performance.
There's an interesting behavior for transactions that are very small. If on any day the allocation is smaller by min_value
for any of legs, the transaction on that day is not generated and put aside in full. Thus, such small transactions tend to happen once in a while with min_value
amount. Even more funny, if there are more than one posting with small amount, those postings keep their "put aside" values seperaly - it may happen that at some day there will be a transaction with both of postings, effectively giving double of min_value
. It may be hard to those little postings in reports, and I doubt that anyone would care about them at all, the best place to look at is the source.
max_new_tx
Details: - Type: integer
Caps the max new transactions generated for one entry. By default set to 9999, looks like working but is not tested.
suffix
Details: - Type: string
Suffix is string appended to created transaction annotations. It has two variables within in the given order:
- n-th transaction
- total amount of transactions.
tag
Details: - Type: string
String that as tag will be applied to all created transactions.
Development
The source contains five files - one per plugin and commons. Plugins have very similar structure in pairs: spread is similar to depreciate, and recur is similar to split.
Please see Makefile and inline comments.
Note: there's a branch single-plugin-refactor
that's a up-for-grabs WIP based on v2 by @benedictvh. See #8 #9, #12 for details.