hledger-elster generates German tax exports for ELSTER.de from an hledger journal.
Required tools:
hledger1.52.1
Download the standalone executable for your platform from the
latest release
and put it on PATH:
curl -L \
-o /tmp/hledger-elster \
https://github.com/roschaefer/hledger-elster/releases/download/latest/hledger-elster-linux-x86_64
install -m 0755 /tmp/hledger-elster ~/.local/bin/hledger-elsterFor macOS on Apple Silicon, use the hledger-elster-macos-arm64 asset instead.
Run it on a journal, here the example journal. It prints each tax year it exports:
$ hledger-elster -f examples/ledger/hledger.journal
2024
2025
Outputs are written under data/exports/<year>/ by default:
$ ls data/exports/2024
herleitung
steuererklaerung
steuererklaerung.xlsx
steuererklaerung/: summary CSV files, one per form, also bundled as tabs ofsteuererklaerung.xlsxherleitung/: detailed audit trails, one.xlsxper form
Workbook export convention:
- every
name.xlsxworkbook has a siblingname/directory - each workbook tab is also exported there as a CSV with the corresponding derived filename
$ ls data/exports/2024/herleitung data/exports/2024/steuererklaerung
data/exports/2024/herleitung:
einkommensteuer
einkommensteuer.xlsx
einnahmen-ueberschuss-rechnung
einnahmen-ueberschuss-rechnung.xlsx
umsatzsteuer
umsatzsteuer.xlsx
data/exports/2024/steuererklaerung:
einkommensteuer.csv
einnahmen-ueberschuss-rechnung.csv
umsatzsteuer.csv
Arguments:
-f,--file: input journal, with the same meaning ashledger -f-o,--output-dir: output directory for generated tax artifacts--config: TOML config file for user-specific tax adjustments (see Config file)
This tool currently targets my own situation: I am a software developer working
remotely as a freelancer (selbststΓ€ndig) and as a Freiberufler. I do not pay
Gewerbesteuer, but I do need to file EΓR, USt, and ESt.
I am not an expert in German tax law, and calculations may contain errors. Bug fixes and contributions are very welcome, especially for additional tax scenarios. The goal is to cover more cases over time through executable examples.
Executable Scrut specifications
live in tests/specifications/. This README is one
too: its scrut blocks run against the example journal,
so the commands and outputs shown above stay accurate. Run them all with
just test-scrut.
Specs:
- Configuration
- CSV/xlsx equivalence
- Traceability
- Export hygiene
- Business expenses and income
- VAT payments and settlements
- VAT reverse charge
- Business vs. private accounts
- Health care and insurance
- GWG and AfA
- Donations
- Git commit evidence
- Reconciliation library
Enter the reproducible development shell, which includes Rust, hledger,
Just, Scrut, and the linting tools:
nix developAlternatively, install a Rust toolchain,
hledger,
Just, and
Scrut yourself.
git clone git@github.com:roschaefer/hledger-elster.git
cd hledger-elster
cargo build --release
cargo testjust check runs the same formatting, lint, and test gates as CI.
The tool reads account-level metadata from hledger account directives (; comments).
All tags are prefixed with elster_. Tags fall into three categories:
- Routing tags β which tax form an account belongs to and how it is grouped there
- Calculation tags β how amounts are transformed before they appear in the output
- Infrastructure tags β account classification used by the ingestion layer
$ write_file accounts.journal <<'EOF'
> ; ββ Payment accounts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
> account assets:bank:business ; elster_account:business, elster_item:GeschΓ€ftskonto
> account assets:bank:private ; elster_account:private, elster_item:Girokonto
>
> ; ββ Business income (EΓR + USt) βββββββββββββββββββββββββββββββββββββββββββββββ
> account income:business ; elster_form:einnahmenueberschussrechnung, elster_vat:contains_vat, elster_vat_rate:0.19, elster_item:Betriebseinnahmen
>
> ; ββ Business expenses (EΓR) βββββββββββββββββββββββββββββββββββββββββββββββββββ
> ; Base account sets defaults; sub-accounts inherit and override.
> account expenses:business ; elster_form:einnahmenueberschussrechnung, elster_deduction:full, elster_vat:contains_vat, elster_vat_rate:0.19, elster_input_vat_share:1.00
> account expenses:business:hosting ; elster_item:Serverkosten, elster_section:Bezogene Fremdleistungen
> account expenses:business:education ; elster_item:Fortbildung, elster_section:Fortbildungskosten
>
> ; Proportional deduction (e.g. phone: 20 % business use)
> account expenses:phone ; elster_form:einnahmenueberschussrechnung, elster_deduction:proportional, elster_expense_share:0.20, elster_vat:contains_vat, elster_input_vat_share:0.20, elster_vat_rate:0.19, elster_item:Mobiltelefon, elster_section:Arbeitsmittel
>
> ; Depreciable asset (AfA)
> account expenses:hardware:computer ; elster_form:einnahmenueberschussrechnung, elster_vat:contains_vat, elster_vat_rate:0.19, elster_afa_years:3, elster_item:Computer-Kauf, elster_section:Arbeitsmittel
>
> ; ββ Private expenses (ESt) ββββββββββββββββββββββββββββββββββββββββββββββββββββ
> account expenses:insurance ; elster_form:einkommensteuer
> account expenses:insurance:health:kv ; elster_item:Krankenversicherung, elster_section:Vorsorgeaufwand
> account expenses:insurance:health:pv ; elster_item:Pflegeversicherung, elster_section:Vorsorgeaufwand
>
> ; Donations are ordinary ESt accounts grouped by a user-defined section.
> ; Child accounts without their own item inherit this item and are summed into one row.
> account expenses:charity ; elster_form:einkommensteuer, elster_item:Spenden, elster_section:Sonderausgaben
> account expenses:charity:drk
> account expenses:charity:unicef
>
> ; Use manual calculation for cases the tool should list but not calculate.
> account expenses:politics:party ; elster_form:einkommensteuer, elster_item:Parteispende - Β§34g/Β§10b manuell berechnen, elster_section:Sonderausgaben, elster_calculation:manual
>
> ; ββ Tax payments ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
> account expenses:taxes:einkommensteuer:vorauszahlung ; elster_role:income_tax_advance, elster_item:ESt-Vorauszahlung
> account expenses:taxes:einkommensteuer:abschlusszahlung ; elster_role:income_tax_final, elster_item:ESt-Abschlusszahlung
> account expenses:taxes:umsatzsteuer:vorauszahlung ; elster_role:vat_advance
> account expenses:taxes:umsatzsteuer:vorauszahlung:2024 ; elster_period:2024
> account expenses:taxes:umsatzsteuer:vorauszahlung:2025 ; elster_period:2025
> EOF
hledger reads the tags from the directives, and child accounts inherit them.
For example, these accounts carry an elster_role:
$ hledger -f accounts.journal accounts tag:elster_role
expenses:taxes:einkommensteuer:vorauszahlung
expenses:taxes:einkommensteuer:abschlusszahlung
expenses:taxes:umsatzsteuer:vorauszahlung
expenses:taxes:umsatzsteuer:vorauszahlung:2024
expenses:taxes:umsatzsteuer:vorauszahlung:2025
User-specific tax assumptions that are not ledger transactions live in a TOML config file. Generate the default config with:
$ hledger-elster init-config --output elster.toml
The default config enables the Home-Office-Pauschale and uses the maximum number of days for each supported year, because this adjustment is easy to forget:
$ cat elster.toml
[euer.home_office_pauschale]
enabled = true
default_days = "max"
# Set per-year days when the default does not match your situation.
# 2020-2022: 5 EUR/day, capped at 600 EUR.
# 2023+: 6 EUR/day, capped at 1260 EUR.
[euer.home_office_pauschale.days]
# 2024 = 210
Use enabled = false for journal-only exports, or set per-year days when the
maximum does not match your situation. Pass the file with --config, and
choose another output directory with -o:
$ hledger-elster -f examples/ledger/hledger.journal --config elster.toml -o elster-out
2024
2025
| Tag | Values | Meaning |
|---|---|---|
elster_account |
business | private |
Marks a payment account as belonging to the business or private sphere. Drives the drawing/contribution fallback: any unclassified outflow from a business account is counted as an Entnahme in the EΓR. |
elster_role |
income_tax_advance | income_tax_final |
Marks ESt payment accounts. Postings appear in the ESt summary, separated by advance vs. final settlement. |
elster_role |
tax_payment |
Generic parent role for all tax payments. Prevents tax outflows from being counted as Entnahmen in the EΓR. |
elster_role |
vat_payment |
Marks USt Abschlusszahlung accounts. Postings flow into the EΓR (line 57: gezahlte Umsatzsteuer) and the USt report. |
elster_role |
vat_advance |
Marks USt Vorauszahlung accounts. Requires elster_period on sub-accounts for correct fiscal-year attribution. |
elster_form |
einnahmenueberschussrechnung |
Marks an account as belonging to the EΓR. Income accounts flow into Betriebseinnahmen; expense accounts flow into Betriebsausgaben. VAT handling is controlled by elster_vat and elster_vat_rate; deduction treatment by the calculation tags below. The USt export is derived from these EΓR VAT fields and VAT payment roles. |
elster_form |
einkommensteuer |
Marks an account as belonging to the ESt. The account appears under the user-defined elster_section; postings from a business source account are additionally counted as Entnahmen in the EΓR. |
elster_section |
free text | User-defined grouping within a form (for example Sonderausgaben for donations or Arbeitsmittel for EΓR expenses). The code does not interpret specific section names. |
elster_item |
free text | Report item shown as an output row. Use it to translate technical account names and to define aggregation boundaries: child accounts without their own elster_item are summed into the inherited parent item; child accounts with their own elster_item appear separately. |
elster_period |
YYYY |
On USt Vorauszahlung sub-accounts: the fiscal year the payment belongs to, regardless of when the transaction occurred. Required on every vat_advance sub-account. |
| Tag | Values | Meaning |
|---|---|---|
elster_vat |
contains_vat |
The booked amount is gross and contains VAT. elster_vat_rate splits the amount into net and VAT. |
elster_vat |
reverse_charge_eu | reverse_charge_non_eu |
The booked amount is net. elster_vat_rate calculates German VAT on top for the USt reverse-charge rows. |
elster_vat |
not_applicable |
No VAT calculation applies. Do not set elster_vat_rate or elster_input_vat_share. |
elster_vat_rate |
0.19 | 0.07 |
German VAT rate used by elster_vat:contains_vat and reverse-charge modes. |
elster_deduction |
full |
The full net amount is deductible as a business expense. |
elster_deduction |
proportional |
Only a fraction is deductible. Set elster_expense_share; set elster_input_vat_share too when the deductible input VAT share differs. |
elster_deduction |
non_deductible |
Show an EΓR expense row but count 0.00 toward deductible business expenses and input VAT. |
elster_deduction |
afa |
Triggers straight-line depreciation. Set elster_afa_years to the useful life. Net cost above β¬800 is depreciated; at or below β¬800 it is treated as full (GWG). |
elster_expense_share |
decimal (e.g. 0.20) |
Fraction of the net amount that is a deductible business expense. Only effective when elster_deduction:proportional. |
elster_input_vat_share |
decimal (e.g. 0.20) |
Fraction of input VAT that is deductible. Defaults to elster_expense_share when omitted. Hospitality can use elster_expense_share:0.70 with elster_input_vat_share:1.00. |
elster_afa_years |
integer (e.g. 3) |
Useful life in years for straight-line depreciation. Only effective when elster_deduction:afa. |
elster_calculation |
manual |
Lists the account in the form but writes MANUAL instead of a calculated amount. The Herleitung still shows the booked payment amount. Use this for unimplemented or externally calculated cases such as political party donations. |
These are set in transaction or posting comments, not in account directives.
| Tag | Value | Meaning |
|---|---|---|
elster_role |
ignore |
Excludes a transaction or posting from all tax calculations. Useful for internal transfers that would otherwise be misclassified. |
elster_period |
YYYY |
On individual postings: overrides the fiscal year for period attribution (same semantics as the account-level tag). |
The ingestion layer assigns these roles automatically; they never appear in
accounts.journal.
| Role | Condition | Effect |
|---|---|---|
drawing |
Outflow from a business account with no matching elster_form or elster_role |
Counted as Entnahme in the EΓR. |
contribution |
Inflow to a business account with no matching elster_form or elster_role |
Counted as Einlage in the EΓR. |
Private expenses tagged with elster_form:einkommensteuer and paid from a
business account are also counted as Entnahmen, even though they carry an
explicit form tag. The EΓR Entnahmen line therefore equals the sum of all
unclassified business-account outflows plus all private-form expenses paid from
the business account.