Record prepaid costs as assets, then release each month's share to expense with ordinary Beancount transactions. The script below prints complete, reproducible ledgers for insurance, software, a retainer, and equipment depreciation. It needs Python 3 and no third-party libraries.
For example, a six-month car insurance policy costs $600 upfront. Six monthly releases of $100 separate the cash payment from expense recognition and help you budget effectively.
What is Amortization?
Amortization spreads the cost of an intangible asset or prepaid item over the periods it benefits. These examples use a simple monthly convention for management reporting. They do not determine a tax deduction or a statutory depreciation method.
The Beancount Solution: fava.plugins.amortize_over
Compatibility checked 2026-09-07: fava.plugins.amortize_over is absent from stock Fava 1.30.16's plugin directory. Adding its plugin directive to that stock installation produces an import error. The portable examples here were tested with Beancount 3.2.3, beanquery 0.2.0, and Fava 1.30.16.
Beancount.io's custom source does contain an amortize_over implementation. It expects a separate payment and a schedule transaction with exactly two postings and amortize_months metadata. Its monthly dates follow the schedule's original day, and it omits future copies at load time. Its rounding rule distributes cents across the remaining periods. This source evidence does not establish which version a hosted deployment currently runs. The explicit schedules below have their own stated dates and final-period rounding rule.
Step 1: Enable the Plugin
There is no plugin to enable for this workflow. Save the Python block in step 3 as amortize.py. Install uv to run the validation and query commands with the versions used here. Fava is only needed if you want the web reports.
The script writes ordinary transactions to standard output. Redirect that output to a new example file, inspect it, and validate it before incorporating entries into your books. Rerunning with > replaces that generated example file; appending with >> would duplicate transactions.
Step 2: Structure Your Accounts
Each generated file is a standalone example ledger. The insurance example opens these accounts and supplies an illustrative $600 opening bank balance before paying the premium:
| Account | Purpose |
|---|---|
Assets:Bank:Checking | Cash used for the payment |
Assets:Prepaid:Insurance | Premium awaiting expense recognition |
Expenses:Insurance:Auto | Monthly insurance expense |
Equity:Opening-Balances | Counterpart of the example's starting cash |
When adapting the generated transactions to an existing ledger, use your actual account opens and bank balance. Do not add the example's opening cash to an already funded account, or record a purchase again if it is already in your books.
Step 3: Record the Amortized Transaction
Save this complete program as amortize.py. Each scenario specifies the payment date, number of monthly releases, purchase asset, expense account, and account credited by the releases. The first release is on the payment date; later releases fall on the same day of subsequent months, clamped to month-end if necessary.
import argparse
import calendar
from datetime import date
from decimal import Decimal, ROUND_HALF_EVEN
# amount, months, payment date, purchase asset, expense, release credit account
SCENARIOS = {
"insurance": (
"600.00", 6, "2024-06-01", "Assets:Prepaid:Insurance",
"Expenses:Insurance:Auto", "Assets:Prepaid:Insurance",
),
"software": (
"1200.00", 12, "2024-01-15", "Assets:Prepaid:Software",
"Expenses:Software:Adobe", "Assets:Prepaid:Software",
),
"retainer": (
"4500.00", 3, "2024-01-05", "Assets:Prepaid:Professional",
"Expenses:Professional:Legal", "Assets:Prepaid:Professional",
),
"equipment": (
"3000.00", 36, "2024-01-20", "Assets:Equipment:Computers:Cost",
"Expenses:Depreciation:Computers", "Assets:Equipment:Computers:AccumDep",
),
}
parser = argparse.ArgumentParser()
parser.add_argument("scenario", choices=SCENARIOS)
args = parser.parse_args()
amount, months, paid, asset, expense, credit = SCENARIOS[args.scenario]
total = Decimal(amount)
start = date.fromisoformat(paid)
monthly = (total / months).quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
bank = "Assets:Bank:Checking"
equity = "Equity:Opening-Balances"
def transaction(day, narration, debit, credit_account, value):
print(f'\n{day} * "{narration}"')
print(f" {debit} {value:.2f} USD")
print(f" {credit_account} {-value:.2f} USD")
print('option "operating_currency" "USD"')
for account in sorted({bank, equity, asset, expense, credit}):
print(f"2024-01-01 open {account} USD")
transaction("2024-01-01", "Example opening cash", bank, equity, total)
transaction(paid, f"Pay {args.scenario}", asset, bank, total)
for index in range(months):
year, month_index = divmod(start.year * 12 + start.month - 1 + index, 12)
month = month_index + 1
day = min(start.day, calendar.monthrange(year, month)[1])
release_date = date(year, month, day)
# Put the entire rounding remainder into the final release exactly once.
release = monthly if index < months - 1 else total - monthly * (months - 1)
transaction(release_date, f"{args.scenario} release {index + 1}/{months}",
expense, credit, release)Run this command block from the directory containing amortize.py:
python3 amortize.py insurance > insurance.beancount
uvx --from beancount-io==0.1.0 bea --file insurance.beancount checkThe payment credits checking by 600.00 USD and debits prepaid insurance by the same amount. Each release debits expense and credits prepaid insurance by 100.00 USD. There are six releases, on June 1, July 1, August 1, September 1, October 1, and November 1, 2024. The prepaid balance is 500.00 USD after June's release, 400.00 USD after July's, and zero after November's.
Practical Examples
Annual Software License
Use the same saved script to generate a separate standalone software example:
python3 amortize.py software > software.beancount
uvx --from beancount-io==0.1.0 bea --file software.beancount checkThe $1,200 license payment is on January 15, 2024. Twelve releases of 100.00 USD run from January 15 through December 15, 2024. This example recognizes one full month at each monthly start; it does not prorate January. After the last release, Assets:Prepaid:Software is zero and Expenses:Software:Adobe totals 1200.00 USD.
Professional Services Retainer
This separate example treats the $4,500 January 5 payment as a prepaid fee consumed equally over January, February, and March:
python3 amortize.py retainer > retainer.beancount
uvx --from beancount-io==0.1.0 bea --file retainer.beancount checkThree releases of 1500.00 USD, dated January 5, February 5, and March 5, 2024, leave Assets:Prepaid:Professional at zero and Expenses:Professional:Legal at 4500.00 USD. For a retainer earned as services are delivered, replace this assumption with the actual service schedule.
Equipment Depreciation (Fixed Assets)
Depreciation for fixed assets uses a contra-asset account to preserve the purchase cost. This example assumes a three-year useful life, zero residual value, and full monthly recognition starting on January 20, 2024:
python3 amortize.py equipment > equipment.beancount
uvx --from beancount-io==0.1.0 bea --file equipment.beancount checkThe purchase debits Assets:Equipment:Computers:Cost by 3000.00 USD once. The 36 releases debit Expenses:Depreciation:Computers and credit Assets:Equipment:Computers:AccumDep. Releases 1–35 are 83.33 USD; release 36 on December 20, 2026 is 83.45 USD. Thus 35 × 83.33 + 83.45 = 3000.00. Using 83.33 USD for all 36 would leave 0.12 USD undepreciated.
At completion, cost remains 3000.00 USD, accumulated depreciation is -3000.00 USD, and their net book value is zero. The contra-asset account itself does not return to zero while the equipment is still recorded.
Reporting and Verification
1. Monitor Prepaid Balances with Assertions
Beancount checks a balance at the start of its date, before that day's transactions. Append this contextual assertion block to the generated insurance.beancount, then rerun its bea check command:
2024-06-02 balance Assets:Prepaid:Insurance 500.00 USD
2024-07-02 balance Assets:Prepaid:Insurance 400.00 USD
2024-07-02 balance Expenses:Insurance:Auto 200.00 USD
2024-11-02 balance Assets:Prepaid:Insurance 0.00 USD
2024-11-02 balance Expenses:Insurance:Auto 600.00 USDJuly 2 follows two releases, June 1 and July 1. An assertion of 500.00 USD on July 2 is incorrect. These assertions need the generated insurance ledger; they are not standalone transactions.
2. Advanced verification with bea query
With the generated insurance file, this executable command returns cumulative balances through July 1, using an exclusive July 2 cutoff:
uvx --from beancount-io==0.1.0 bea --file insurance.beancount query "SELECT account, sum(position) WHERE date < 2024-07-02 AND account ~ '^(Assets:Prepaid:Insurance|Expenses:Insurance:Auto)$' GROUP BY account ORDER BY account"Expect Assets:Prepaid:Insurance at 400.00 USD and Expenses:Insurance:Auto at 200.00 USD. Grouping by date instead would show each day's movement rather than a cumulative balance.
To view any generated file in the tested local Fava version, use:
uv run --no-project --with beancount==3.2.3 --with beanquery==0.2.0 --with fava==1.30.16 fava insurance.beancountOpen the local URL Fava prints. See the Fava options reference for configuration. Select the reporting period explicitly: the generator emits the entire schedule, including future dates, so an unrestricted total includes future releases. For separate planning files, see the forecast scenario workflow.
Limitations and Final Tips
- Dates are explicit: Changing a purchase date does not silently defer the first release. In this script the first release equals the payment date; adapt the start separately if the service starts later.
- Rounding happens once: The script rounds the regular monthly amount to cents, then puts the remainder in the final release. Keep the full generated schedule to preserve that adjustment.
- Ordinary entries persist: These releases are saved in the generated file. Review and record them once. Do not also enable automatic amortization for the same cost, or expense would be recognized twice.
- Choose the accounting schedule: These are management examples, not jurisdiction-specific tax rules. For irregular benefits or daily proration, calculate and record the actual amounts and dates instead.