beancount-cli 0.2.14__tar.gz → 0.2.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/CHANGELOG.md +15 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/PKG-INFO +25 -2
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/README.md +23 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/pyproject.toml +2 -2
- beancount_cli-0.2.16/src/beancount_cli/__init__.py +1 -0
- beancount_cli-0.2.16/src/beancount_cli/commands/account.py +178 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/commodity.py +14 -28
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/common.py +0 -7
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/transaction.py +21 -19
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/models.py +6 -2
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/services.py +0 -7
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_cli.py +14 -7
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_coverage_gap.py +12 -6
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_models.py +7 -7
- beancount_cli-0.2.14/src/beancount_cli/__init__.py +0 -1
- beancount_cli-0.2.14/src/beancount_cli/commands/account.py +0 -202
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/.gitignore +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/LICENSE +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/adapters.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/cli.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/__init__.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/price.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/report.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/root.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/config.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/formatting.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/py.typed +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/conftest.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/smoke_test.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_advanced.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_bql.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_config.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_price_cash_currency.py +0 -0
- {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_services.py +0 -0
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.16] - 2026-06-12
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Upgraded `agentyper` to `0.1.17`.
|
|
12
|
+
|
|
13
|
+
## [0.2.15] - 2026-06-12
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Upgraded `agentyper` to `0.1.16`, which provides `bean exec` as a built-in JSONL batch command (replaces the custom implementation).
|
|
17
|
+
- Replaced `--input` / `-i` JSON-stdin flags with individual CLI flags on `transaction add`, `account create`, `account balance`, `account pad-balance`, and `commodity create`.
|
|
18
|
+
- `bean exec` now maps JSONL payload fields to individual command flags automatically via agentyper's built-in dispatch.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- `--input` / `-i` flag removed from all mutating commands; use `bean exec` for batch pipelines.
|
|
22
|
+
|
|
8
23
|
## [0.2.14] - 2026-06-02
|
|
9
24
|
|
|
10
25
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beancount-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
4
4
|
Summary: A CLI tool to manage Beancount ledgers
|
|
5
5
|
Project-URL: Homepage, https://github.com/romamo/beancount-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/romamo/beancount-cli
|
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
22
22
|
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist: agentyper==0.1.
|
|
23
|
+
Requires-Dist: agentyper==0.1.17
|
|
24
24
|
Requires-Dist: beancount==3.2.3
|
|
25
25
|
Requires-Dist: beanprice2>=2.1.0
|
|
26
26
|
Requires-Dist: beanquery>=0.1.0
|
|
@@ -50,6 +50,7 @@ A robust command-line interface and Python library for programmatically managing
|
|
|
50
50
|
- Global output formatting: `--format` support (`table`, `json`, `csv`) for all data commands.
|
|
51
51
|
- **Reporting**: Generate balance, holding, and audit reports with multi-currency conversion.
|
|
52
52
|
- **Composability**: Built for Unix piping (`json` | `csv`) and batch processing via STDIN.
|
|
53
|
+
- **JSONL Stream Execution**: `bean exec` dispatches a mixed-command JSONL stream to the ledger; `--dry-run` previews the result without writing.
|
|
53
54
|
- **Configuration**: Custom Beancount directives for routing new entries to specific files.
|
|
54
55
|
- **Tab Completion**: We provide tab completions for bash and zsh.
|
|
55
56
|
|
|
@@ -165,6 +166,28 @@ cat tx.json | bean transaction add main.beancount --json -
|
|
|
165
166
|
bean transaction add main.beancount --json ... --draft
|
|
166
167
|
```
|
|
167
168
|
|
|
169
|
+
### JSONL Stream Execution
|
|
170
|
+
|
|
171
|
+
Use `bean exec` to dispatch a mixed-command JSONL stream (one JSON object per line) to the ledger in a single pass.
|
|
172
|
+
|
|
173
|
+
Each line must contain a `_cmd` field mapping to any CLI command (`transaction.add`, `account.create`, `commodity.create`, etc.). Use `_opts` to pass CLI flags that cannot go through the payload.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Write a mixed stream to the ledger
|
|
177
|
+
cat commands.jsonl | uv run bean exec
|
|
178
|
+
|
|
179
|
+
# Preview without writing
|
|
180
|
+
cat commands.jsonl | uv run bean exec --dry-run
|
|
181
|
+
|
|
182
|
+
# Continue after errors, collect all failures
|
|
183
|
+
cat commands.jsonl | uv run bean exec --ignore-errors 2>errors.log
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Example JSONL line:
|
|
187
|
+
```json
|
|
188
|
+
{"_cmd": "transaction.add", "_opts": {"draft": true}, "date": "2024-01-01", "narration": "Buy coffee", "postings": [{"account": "Expenses:Food", "units": {"number": 5, "currency": "USD"}}, {"account": "Assets:Cash", "units": {"number": -5, "currency": "USD"}}]}
|
|
189
|
+
```
|
|
190
|
+
|
|
168
191
|
### Manage Accounts & Commodities
|
|
169
192
|
|
|
170
193
|
All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN. Use `--target` to override the destination file.
|
|
@@ -19,6 +19,7 @@ A robust command-line interface and Python library for programmatically managing
|
|
|
19
19
|
- Global output formatting: `--format` support (`table`, `json`, `csv`) for all data commands.
|
|
20
20
|
- **Reporting**: Generate balance, holding, and audit reports with multi-currency conversion.
|
|
21
21
|
- **Composability**: Built for Unix piping (`json` | `csv`) and batch processing via STDIN.
|
|
22
|
+
- **JSONL Stream Execution**: `bean exec` dispatches a mixed-command JSONL stream to the ledger; `--dry-run` previews the result without writing.
|
|
22
23
|
- **Configuration**: Custom Beancount directives for routing new entries to specific files.
|
|
23
24
|
- **Tab Completion**: We provide tab completions for bash and zsh.
|
|
24
25
|
|
|
@@ -134,6 +135,28 @@ cat tx.json | bean transaction add main.beancount --json -
|
|
|
134
135
|
bean transaction add main.beancount --json ... --draft
|
|
135
136
|
```
|
|
136
137
|
|
|
138
|
+
### JSONL Stream Execution
|
|
139
|
+
|
|
140
|
+
Use `bean exec` to dispatch a mixed-command JSONL stream (one JSON object per line) to the ledger in a single pass.
|
|
141
|
+
|
|
142
|
+
Each line must contain a `_cmd` field mapping to any CLI command (`transaction.add`, `account.create`, `commodity.create`, etc.). Use `_opts` to pass CLI flags that cannot go through the payload.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Write a mixed stream to the ledger
|
|
146
|
+
cat commands.jsonl | uv run bean exec
|
|
147
|
+
|
|
148
|
+
# Preview without writing
|
|
149
|
+
cat commands.jsonl | uv run bean exec --dry-run
|
|
150
|
+
|
|
151
|
+
# Continue after errors, collect all failures
|
|
152
|
+
cat commands.jsonl | uv run bean exec --ignore-errors 2>errors.log
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example JSONL line:
|
|
156
|
+
```json
|
|
157
|
+
{"_cmd": "transaction.add", "_opts": {"draft": true}, "date": "2024-01-01", "narration": "Buy coffee", "postings": [{"account": "Expenses:Food", "units": {"number": 5, "currency": "USD"}}, {"account": "Assets:Cash", "units": {"number": -5, "currency": "USD"}}]}
|
|
158
|
+
```
|
|
159
|
+
|
|
137
160
|
### Manage Accounts & Commodities
|
|
138
161
|
|
|
139
162
|
All creation commands (`transaction add`, `account create`, `commodity create`) support batch processing via JSON arrays on STDIN. Use `--target` to override the destination file.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "beancount-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.16"
|
|
4
4
|
description = "A CLI tool to manage Beancount ledgers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
"python-dateutil>=2.8.2",
|
|
16
16
|
"beanprice2>=2.1.0",
|
|
17
17
|
"pydantic-settings==2.14.1",
|
|
18
|
-
"agentyper==0.1.
|
|
18
|
+
"agentyper==0.1.17",
|
|
19
19
|
]
|
|
20
20
|
keywords = ["beancount", "cli", "accounting", "automation", "agent"]
|
|
21
21
|
classifiers = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.16"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from datetime import date
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import agentyper as typer
|
|
6
|
+
from beancount.core import data
|
|
7
|
+
from beancount.parser import printer
|
|
8
|
+
|
|
9
|
+
from beancount_cli.adapters import to_core_balance, to_core_pad
|
|
10
|
+
from beancount_cli.commands.common import (
|
|
11
|
+
_is_table_format,
|
|
12
|
+
console,
|
|
13
|
+
get_ledger_file,
|
|
14
|
+
)
|
|
15
|
+
from beancount_cli.models import AccountModel, BalanceModel, PadBalanceModel
|
|
16
|
+
from beancount_cli.services import AccountService
|
|
17
|
+
|
|
18
|
+
app = typer.Agentyper(help="Manage accounts.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_open(m: AccountModel) -> str:
|
|
22
|
+
return printer.format_entry(
|
|
23
|
+
data.Open(
|
|
24
|
+
meta={},
|
|
25
|
+
date=m.open_date,
|
|
26
|
+
account=str(m.name),
|
|
27
|
+
currencies=[str(c) for c in m.currencies],
|
|
28
|
+
booking=None,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command(name="list")
|
|
34
|
+
def account_list(
|
|
35
|
+
file: Path | None = typer.Option(
|
|
36
|
+
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
37
|
+
),
|
|
38
|
+
):
|
|
39
|
+
"""List all accounts."""
|
|
40
|
+
actual_file = get_ledger_file(file)
|
|
41
|
+
service = AccountService(actual_file)
|
|
42
|
+
accounts = service.list_accounts()
|
|
43
|
+
|
|
44
|
+
if _is_table_format():
|
|
45
|
+
data = [
|
|
46
|
+
{
|
|
47
|
+
"Account": acc.name,
|
|
48
|
+
"Open Date": str(acc.open_date),
|
|
49
|
+
"Currencies": ", ".join(acc.currencies),
|
|
50
|
+
}
|
|
51
|
+
for acc in accounts
|
|
52
|
+
]
|
|
53
|
+
typer.output(data, title=f"Accounts ({len(accounts)})")
|
|
54
|
+
else:
|
|
55
|
+
typer.output(accounts, title=f"Accounts ({len(accounts)})")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command(name="create", mutating=True)
|
|
59
|
+
def account_create(
|
|
60
|
+
file: Path | None = typer.Option(
|
|
61
|
+
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
62
|
+
),
|
|
63
|
+
name: str = typer.Option(..., "--name", "-n", help="Account name (e.g. Assets:Bank)"),
|
|
64
|
+
currency_opt: str | None = typer.Option(
|
|
65
|
+
None, "--currency", "-c", help="Currencies (comma-separated)"
|
|
66
|
+
),
|
|
67
|
+
open_date: str | None = typer.Option(None, "--date", "-d", help="Open date (YYYY-MM-DD)"),
|
|
68
|
+
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
69
|
+
dry_run: bool = False,
|
|
70
|
+
):
|
|
71
|
+
"""Create a new account."""
|
|
72
|
+
d = date.fromisoformat(open_date) if open_date else date.today()
|
|
73
|
+
|
|
74
|
+
currencies = [c.strip() for c in currency_opt.split(",")] if currency_opt else []
|
|
75
|
+
model = AccountModel(name=name, open_date=d, currencies=currencies)
|
|
76
|
+
if dry_run:
|
|
77
|
+
sys.stdout.write(_format_open(model) + "\n")
|
|
78
|
+
return
|
|
79
|
+
try:
|
|
80
|
+
AccountService(get_ledger_file(file)).create_account(model, target_file=target)
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
typer.exit_error(str(e))
|
|
83
|
+
console.print(f"[green]Account {name} created.[/green]")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command(name="balance", mutating=True)
|
|
87
|
+
def account_balance(
|
|
88
|
+
file: Path | None = typer.Option(
|
|
89
|
+
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
90
|
+
),
|
|
91
|
+
account: str = typer.Option(..., "--account", help="Account name (e.g. Assets:Bank)"),
|
|
92
|
+
date: str = typer.Option(..., "--date", help="Balance date (YYYY-MM-DD)"),
|
|
93
|
+
amount: str = typer.Option(..., "--amount", help="Balance amount (e.g. 1000.00)"),
|
|
94
|
+
currency: str = typer.Option(..., "--currency", "-c", help="Currency code (e.g. USD)"),
|
|
95
|
+
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
96
|
+
dry_run: bool = False,
|
|
97
|
+
):
|
|
98
|
+
"""Add a balance directive for an account."""
|
|
99
|
+
model = BalanceModel(
|
|
100
|
+
account=account,
|
|
101
|
+
date=date,
|
|
102
|
+
amount={"number": amount, "currency": currency},
|
|
103
|
+
)
|
|
104
|
+
if dry_run:
|
|
105
|
+
sys.stdout.write(printer.format_entry(to_core_balance(model)) + "\n")
|
|
106
|
+
return
|
|
107
|
+
actual_file = get_ledger_file(file)
|
|
108
|
+
service = AccountService(actual_file)
|
|
109
|
+
service.add_balance(model, target_file=target)
|
|
110
|
+
console.print(f"[green]Balance check for {model.account} added.[/green]")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command(name="pad-balance", mutating=True)
|
|
114
|
+
def account_pad_balance(
|
|
115
|
+
file: Path | None = typer.Option(
|
|
116
|
+
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
117
|
+
),
|
|
118
|
+
account: str = typer.Option(
|
|
119
|
+
..., "--account", help="Account to adjust (e.g. Assets:BE:Wise:EUR)"
|
|
120
|
+
),
|
|
121
|
+
amount: str = typer.Option(..., "--amount", help="Target balance amount (e.g. 1777.00)"),
|
|
122
|
+
currency: str = typer.Option(
|
|
123
|
+
..., "--currency", "-c", help="Currency of the target balance (e.g. EUR)"
|
|
124
|
+
),
|
|
125
|
+
pad_account: str = typer.Option(
|
|
126
|
+
"Expenses:Other",
|
|
127
|
+
"--pad-account",
|
|
128
|
+
"-p",
|
|
129
|
+
help="Account to absorb the difference (default: Expenses:Other)",
|
|
130
|
+
),
|
|
131
|
+
balance_date: str | None = typer.Option(
|
|
132
|
+
None,
|
|
133
|
+
"--date",
|
|
134
|
+
"-d",
|
|
135
|
+
help="Date of the balance assertion (YYYY-MM-DD). Defaults to today.",
|
|
136
|
+
),
|
|
137
|
+
pad_date: str | None = typer.Option(
|
|
138
|
+
None,
|
|
139
|
+
"--pad-date",
|
|
140
|
+
help="Date of the pad directive (YYYY-MM-DD). Defaults to balance-date minus 1 day.",
|
|
141
|
+
),
|
|
142
|
+
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
143
|
+
dry_run: bool = False,
|
|
144
|
+
):
|
|
145
|
+
"""Adjust an account balance using a Pad + Balance directive pair.
|
|
146
|
+
|
|
147
|
+
Beancount inserts a synthetic transaction to bring ACCOUNT to AMOUNT
|
|
148
|
+
on BALANCE-DATE. The adjustment is automatically booked to PAD-ACCOUNT.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
uv run bean account pad-balance \\
|
|
152
|
+
--account Assets:BE:Wise:EUR \\
|
|
153
|
+
--amount 1777 --currency EUR \\
|
|
154
|
+
--pad-account Expenses:Other
|
|
155
|
+
"""
|
|
156
|
+
b_date = date.fromisoformat(balance_date) if balance_date else date.today()
|
|
157
|
+
p_date = date.fromisoformat(pad_date) if pad_date else None
|
|
158
|
+
|
|
159
|
+
model = PadBalanceModel(
|
|
160
|
+
balance_date=b_date,
|
|
161
|
+
account=account,
|
|
162
|
+
amount={"number": amount, "currency": currency},
|
|
163
|
+
pad_account=pad_account,
|
|
164
|
+
pad_date=p_date,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if dry_run:
|
|
168
|
+
core_pad, core_balance = to_core_pad(model)
|
|
169
|
+
sys.stdout.write(printer.format_entry(core_pad) + "\n")
|
|
170
|
+
sys.stdout.write(printer.format_entry(core_balance) + "\n")
|
|
171
|
+
return
|
|
172
|
+
actual_file = get_ledger_file(file)
|
|
173
|
+
service = AccountService(actual_file)
|
|
174
|
+
service.add_pad_balance(model, target_file=target)
|
|
175
|
+
console.print(
|
|
176
|
+
f"[green]Pad + Balance for {model.account} → {model.amount.number} "
|
|
177
|
+
f"{model.amount.currency} added.[/green]"
|
|
178
|
+
)
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import logging
|
|
3
2
|
import sys
|
|
3
|
+
from datetime import date
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import agentyper as typer
|
|
7
7
|
from beancount.core import data
|
|
8
8
|
from beancount.parser import parser as bp_parser
|
|
9
|
+
from beancount.parser import printer
|
|
9
10
|
|
|
10
11
|
from beancount_cli.commands.common import (
|
|
11
12
|
_is_table_format,
|
|
12
13
|
console,
|
|
13
14
|
error_console,
|
|
14
15
|
get_ledger_file,
|
|
15
|
-
read_json_input,
|
|
16
16
|
read_stdin,
|
|
17
17
|
)
|
|
18
18
|
from beancount_cli.models import CommodityModel
|
|
@@ -158,36 +158,22 @@ def commodity_import(
|
|
|
158
158
|
sys.stdout.write(service._format_commodity_block(c))
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
@app.command(name="create")
|
|
161
|
+
@app.command(name="create", mutating=True)
|
|
162
162
|
def commodity_create(
|
|
163
|
-
currency: str
|
|
163
|
+
currency: str = typer.Argument(..., help="Currency code (e.g. USD)"),
|
|
164
164
|
file: Path | None = typer.Option(
|
|
165
165
|
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
166
166
|
),
|
|
167
167
|
name: str | None = typer.Option(None, "--name", "-n", help="Full name"),
|
|
168
|
-
|
|
169
|
-
None, "--input", "-i", help="JSON string data (or '-' to read from STDIN)"
|
|
170
|
-
),
|
|
168
|
+
dry_run: bool = False,
|
|
171
169
|
):
|
|
172
170
|
"""Create a new commodity."""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if not curr:
|
|
183
|
-
console.print(f"[yellow]Skipping invalid commodity entry: {item}[/yellow]")
|
|
184
|
-
continue
|
|
185
|
-
meta = {k: v for k, v in item.items() if k not in ("currency", "name")}
|
|
186
|
-
service.create_commodity(curr, name=comm_name, meta=meta)
|
|
187
|
-
console.print(f"[green]Commodity {curr} created.[/green]")
|
|
188
|
-
else:
|
|
189
|
-
if not currency:
|
|
190
|
-
console.print("[red]Error: currency argument is required if not using --input.[/red]")
|
|
191
|
-
sys.exit(typer.EXIT_VALIDATION)
|
|
192
|
-
service.create_commodity(currency, name=name)
|
|
193
|
-
console.print(f"[green]Commodity {currency} created.[/green]")
|
|
171
|
+
if dry_run:
|
|
172
|
+
meta = {"name": name} if name else {}
|
|
173
|
+
sys.stdout.write(
|
|
174
|
+
printer.format_entry(data.Commodity(meta=meta, date=date.today(), currency=currency))
|
|
175
|
+
+ "\n"
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
CommodityService(get_ledger_file(file)).create_commodity(currency, name=name)
|
|
179
|
+
console.print(f"[green]Commodity {currency} created.[/green]")
|
|
@@ -39,13 +39,6 @@ def read_stdin() -> str:
|
|
|
39
39
|
return chunk.decode()
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def read_json_input(json_data: str) -> str:
|
|
43
|
-
"""Read JSON from a string or from STDIN when json_data is '-'."""
|
|
44
|
-
if json_data == "-":
|
|
45
|
-
return sys.stdin.read()
|
|
46
|
-
return json_data
|
|
47
|
-
|
|
48
|
-
|
|
49
42
|
def get_ledger_file(override: str | Path | None = None) -> Path:
|
|
50
43
|
if override:
|
|
51
44
|
return Path(override)
|
|
@@ -2,9 +2,8 @@ import json
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import agentyper as typer
|
|
5
|
-
from pydantic import TypeAdapter
|
|
6
5
|
|
|
7
|
-
from beancount_cli.commands.common import _is_table_format, get_ledger_file
|
|
6
|
+
from beancount_cli.commands.common import _is_table_format, get_ledger_file
|
|
8
7
|
from beancount_cli.models import TransactionModel
|
|
9
8
|
from beancount_cli.services import TransactionService
|
|
10
9
|
|
|
@@ -45,29 +44,32 @@ def tx_list(
|
|
|
45
44
|
typer.output(results, title=f"Transactions ({len(txs)})")
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
@app.command(name="add")
|
|
47
|
+
@app.command(name="add", mutating=True)
|
|
49
48
|
def tx_add(
|
|
50
49
|
file: Path | None = typer.Option(
|
|
51
50
|
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
52
51
|
),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
),
|
|
52
|
+
date: str = typer.Option(..., "--date", help="Transaction date (YYYY-MM-DD)"),
|
|
53
|
+
narration: str = typer.Option(..., "--narration", help="Transaction narration"),
|
|
54
|
+
postings: str = typer.Option(..., "--postings", help="JSON array of posting objects"),
|
|
55
|
+
payee: str | None = typer.Option(None, "--payee", help="Payee name"),
|
|
56
|
+
tags: str = typer.Option("[]", "--tags", help="JSON array of tags"),
|
|
57
|
+
links: str = typer.Option("[]", "--links", help="JSON array of links"),
|
|
56
58
|
draft: bool = typer.Option(False, "--draft", help="Mark as pending (!)"),
|
|
57
59
|
print_only: bool = typer.Option(False, "--print", help="Print only, do not write"),
|
|
60
|
+
dry_run: bool = False,
|
|
58
61
|
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
59
62
|
):
|
|
60
63
|
"""Add a new transaction."""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
service.add_transaction(model, draft=draft, print_only=print_only, target_file=target)
|
|
64
|
+
model = TransactionModel(
|
|
65
|
+
date=date,
|
|
66
|
+
narration=narration,
|
|
67
|
+
payee=payee,
|
|
68
|
+
postings=json.loads(postings),
|
|
69
|
+
tags=set(json.loads(tags)),
|
|
70
|
+
links=set(json.loads(links)),
|
|
71
|
+
)
|
|
72
|
+
service = TransactionService(get_ledger_file(file))
|
|
73
|
+
service.add_transaction(
|
|
74
|
+
model, draft=draft, print_only=print_only or dry_run, target_file=target
|
|
75
|
+
)
|
|
@@ -13,7 +13,11 @@ def validate_account_name(v: Any) -> str:
|
|
|
13
13
|
if not isinstance(v, str):
|
|
14
14
|
raise TypeError("string required")
|
|
15
15
|
if not re.match(r"^[A-Z][A-Za-z0-9\-]*(?::[A-Z0-9][A-Za-z0-9\-]*)+$", v):
|
|
16
|
-
raise ValueError(
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Invalid account name '{v}': must have at least two colon-separated segments, "
|
|
18
|
+
f"the first starting with an uppercase letter, each subsequent segment starting "
|
|
19
|
+
f"with an uppercase letter or digit (e.g. 'Assets:US:Bank:Checking')"
|
|
20
|
+
)
|
|
17
21
|
return v
|
|
18
22
|
|
|
19
23
|
|
|
@@ -124,7 +128,7 @@ class PadBalanceModel(BaseModel):
|
|
|
124
128
|
balance_date: datetime.date
|
|
125
129
|
account: AccountName.Input
|
|
126
130
|
amount: AmountModel
|
|
127
|
-
pad_account: AccountName.Input
|
|
131
|
+
pad_account: AccountName.Input = "Expenses:Other"
|
|
128
132
|
pad_date: datetime.date | None = None
|
|
129
133
|
"""Date for the pad directive. Defaults to balance_date minus one day."""
|
|
130
134
|
meta: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -728,13 +728,6 @@ class AccountService:
|
|
|
728
728
|
|
|
729
729
|
if str(model.account) not in existing:
|
|
730
730
|
raise ValueError(f"Account '{model.account}' does not exist (no Open directive).")
|
|
731
|
-
if str(model.pad_account) not in existing:
|
|
732
|
-
raise ValueError(
|
|
733
|
-
f"Pad account '{model.pad_account}' does not exist (no Open directive). "
|
|
734
|
-
"Create it first with: uv run bean account create --name '"
|
|
735
|
-
+ str(model.pad_account)
|
|
736
|
-
+ "'"
|
|
737
|
-
)
|
|
738
731
|
|
|
739
732
|
core_pad, core_balance = to_core_pad(model)
|
|
740
733
|
pad_str = printer.format_entry(core_pad)
|
|
@@ -53,16 +53,23 @@ def test_transaction_list_fields(temp_beancount_file):
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def test_transaction_add_json(temp_beancount_file):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"narration": "CLI Test",
|
|
59
|
-
"postings": [
|
|
56
|
+
postings = json.dumps(
|
|
57
|
+
[
|
|
60
58
|
{"account": "Assets:Cash", "units": {"number": -10, "currency": "USD"}},
|
|
61
59
|
{"account": "Expenses:Food", "units": {"number": 10, "currency": "USD"}},
|
|
62
|
-
]
|
|
63
|
-
|
|
60
|
+
]
|
|
61
|
+
)
|
|
64
62
|
code, out, err = run_cli(
|
|
65
|
-
"transaction",
|
|
63
|
+
"transaction",
|
|
64
|
+
"add",
|
|
65
|
+
"--file",
|
|
66
|
+
str(temp_beancount_file),
|
|
67
|
+
"--date",
|
|
68
|
+
"2023-12-01",
|
|
69
|
+
"--narration",
|
|
70
|
+
"CLI Test",
|
|
71
|
+
"--postings",
|
|
72
|
+
postings,
|
|
66
73
|
)
|
|
67
74
|
assert code in (0, None)
|
|
68
75
|
|
|
@@ -41,13 +41,19 @@ def test_tx_list_json_format(temp_beancount_file):
|
|
|
41
41
|
assert isinstance(data, (list, dict))
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""Exercises
|
|
46
|
-
payload = jsonlib.dumps(
|
|
47
|
-
[{"name": "Assets:Savings2", "open_date": "2024-01-01", "currencies": ["USD"]}]
|
|
48
|
-
)
|
|
44
|
+
def test_account_create_individual_flags(temp_beancount_file):
|
|
45
|
+
"""Exercises account create with individual flags."""
|
|
49
46
|
code, out, err = _run_cli(
|
|
50
|
-
"account",
|
|
47
|
+
"account",
|
|
48
|
+
"create",
|
|
49
|
+
"--file",
|
|
50
|
+
str(temp_beancount_file),
|
|
51
|
+
"--name",
|
|
52
|
+
"Assets:Savings2",
|
|
53
|
+
"--date",
|
|
54
|
+
"2024-01-01",
|
|
55
|
+
"--currency",
|
|
56
|
+
"USD",
|
|
51
57
|
)
|
|
52
58
|
assert code in (0, None)
|
|
53
59
|
|
|
@@ -37,18 +37,18 @@ def test_account_name_validation():
|
|
|
37
37
|
# Valid
|
|
38
38
|
assert validate_account_name("Assets:Cash") == "Assets:Cash"
|
|
39
39
|
assert validate_account_name("Expenses:Office:Supplies") == "Expenses:Office:Supplies"
|
|
40
|
-
assert (
|
|
41
|
-
validate_account_name("Expenses:Office:7622-Equipment-under-3y")
|
|
42
|
-
== "Expenses:Office:7622-Equipment-under-3y"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
40
|
# Invalid
|
|
46
|
-
with pytest.raises(ValueError, match="Invalid account name
|
|
41
|
+
with pytest.raises(ValueError, match="Invalid account name"):
|
|
47
42
|
validate_account_name("assets:cash")
|
|
48
43
|
|
|
49
|
-
with pytest.raises(ValueError, match="Invalid account name
|
|
44
|
+
with pytest.raises(ValueError, match="Invalid account name"):
|
|
50
45
|
validate_account_name("Assets::Cash")
|
|
51
46
|
|
|
47
|
+
assert (
|
|
48
|
+
validate_account_name("Expenses:Office:7622-Equipment-under-3y")
|
|
49
|
+
== "Expenses:Office:7622-Equipment-under-3y"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
52
|
|
|
53
53
|
def test_currency_code_validation():
|
|
54
54
|
# Valid
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.14"
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
from datetime import date
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import agentyper as typer
|
|
7
|
-
from pydantic import TypeAdapter
|
|
8
|
-
|
|
9
|
-
from beancount_cli.commands.common import (
|
|
10
|
-
_is_table_format,
|
|
11
|
-
console,
|
|
12
|
-
get_ledger_file,
|
|
13
|
-
read_json_input,
|
|
14
|
-
)
|
|
15
|
-
from beancount_cli.models import AccountModel, BalanceModel, PadBalanceModel
|
|
16
|
-
from beancount_cli.services import AccountService
|
|
17
|
-
|
|
18
|
-
app = typer.Agentyper(help="Manage accounts.")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@app.command(name="list")
|
|
22
|
-
def account_list(
|
|
23
|
-
file: Path | None = typer.Option(
|
|
24
|
-
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
25
|
-
),
|
|
26
|
-
):
|
|
27
|
-
"""List all accounts."""
|
|
28
|
-
actual_file = get_ledger_file(file)
|
|
29
|
-
service = AccountService(actual_file)
|
|
30
|
-
accounts = service.list_accounts()
|
|
31
|
-
|
|
32
|
-
if _is_table_format():
|
|
33
|
-
data = [
|
|
34
|
-
{
|
|
35
|
-
"Account": acc.name,
|
|
36
|
-
"Open Date": str(acc.open_date),
|
|
37
|
-
"Currencies": ", ".join(acc.currencies),
|
|
38
|
-
}
|
|
39
|
-
for acc in accounts
|
|
40
|
-
]
|
|
41
|
-
typer.output(data, title=f"Accounts ({len(accounts)})")
|
|
42
|
-
else:
|
|
43
|
-
typer.output(accounts, title=f"Accounts ({len(accounts)})")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@app.command(name="create")
|
|
47
|
-
def account_create(
|
|
48
|
-
file: Path | None = typer.Option(
|
|
49
|
-
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
50
|
-
),
|
|
51
|
-
name: str | None = typer.Option(None, "--name", "-n", help="Account name (e.g. Assets:Bank)"),
|
|
52
|
-
currency_opt: str | None = typer.Option(
|
|
53
|
-
None, "--currency", "-c", help="Currencies (comma-separated)"
|
|
54
|
-
),
|
|
55
|
-
open_date: str | None = typer.Option(None, "--date", "-d", help="Open date (YYYY-MM-DD)"),
|
|
56
|
-
json_data: str | None = typer.Option(
|
|
57
|
-
None, "--input", "-i", help="JSON string data (or '-' to read from STDIN)"
|
|
58
|
-
),
|
|
59
|
-
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
60
|
-
):
|
|
61
|
-
"""Create a new account."""
|
|
62
|
-
actual_file = get_ledger_file(file)
|
|
63
|
-
service = AccountService(actual_file)
|
|
64
|
-
|
|
65
|
-
if json_data:
|
|
66
|
-
data_input = json.loads(read_json_input(json_data))
|
|
67
|
-
if isinstance(data_input, list):
|
|
68
|
-
ta = TypeAdapter(list[AccountModel])
|
|
69
|
-
models = ta.validate_python(data_input)
|
|
70
|
-
for m in models:
|
|
71
|
-
service.create_account(m, target_file=target)
|
|
72
|
-
console.print(f"[green]Account {m.name} created.[/green]")
|
|
73
|
-
else:
|
|
74
|
-
model = AccountModel(**data_input)
|
|
75
|
-
service.create_account(model, target_file=target)
|
|
76
|
-
console.print(f"[green]Account {model.name} created.[/green]")
|
|
77
|
-
else:
|
|
78
|
-
if not name:
|
|
79
|
-
console.print("[red]Error: --name is required if not using --input.[/red]")
|
|
80
|
-
sys.exit(typer.EXIT_VALIDATION)
|
|
81
|
-
|
|
82
|
-
d = date.today()
|
|
83
|
-
if open_date:
|
|
84
|
-
d = date.fromisoformat(open_date)
|
|
85
|
-
|
|
86
|
-
currencies = [c.strip() for c in currency_opt.split(",")] if currency_opt else []
|
|
87
|
-
model = AccountModel(name=name, open_date=d, currencies=currencies)
|
|
88
|
-
service.create_account(model, target_file=target)
|
|
89
|
-
console.print(f"[green]Account {name} created.[/green]")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@app.command(name="balance")
|
|
93
|
-
def account_balance(
|
|
94
|
-
file: Path | None = typer.Option(
|
|
95
|
-
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
96
|
-
),
|
|
97
|
-
json_data: str = typer.Option(
|
|
98
|
-
..., "--input", "-i", help="JSON string data (or '-' to read from STDIN)"
|
|
99
|
-
),
|
|
100
|
-
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
101
|
-
):
|
|
102
|
-
"""Add a balance directive for an account."""
|
|
103
|
-
actual_file = get_ledger_file(file)
|
|
104
|
-
service = AccountService(actual_file)
|
|
105
|
-
|
|
106
|
-
data_input = json.loads(read_json_input(json_data))
|
|
107
|
-
model = BalanceModel(**data_input)
|
|
108
|
-
service.add_balance(model, target_file=target)
|
|
109
|
-
console.print(f"[green]Balance check for {model.account} added.[/green]")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@app.command(name="pad-balance")
|
|
113
|
-
def account_pad_balance(
|
|
114
|
-
file: Path | None = typer.Option(
|
|
115
|
-
None, "--file", "-f", envvar="BEANCOUNT_FILE", help="Main beancount file"
|
|
116
|
-
),
|
|
117
|
-
account: str | None = typer.Option(
|
|
118
|
-
None, "--account", help="Account to adjust (e.g. Assets:BE:Wise:EUR)"
|
|
119
|
-
),
|
|
120
|
-
amount: str | None = typer.Option(
|
|
121
|
-
None, "--amount", help="Target balance amount (e.g. 1777.00)"
|
|
122
|
-
),
|
|
123
|
-
currency: str | None = typer.Option(
|
|
124
|
-
None, "--currency", "-c", help="Currency of the target balance (e.g. EUR)"
|
|
125
|
-
),
|
|
126
|
-
pad_account: str | None = typer.Option(
|
|
127
|
-
None,
|
|
128
|
-
"--pad-account",
|
|
129
|
-
"-p",
|
|
130
|
-
help="Account to absorb the difference (e.g. Expenses:Other)",
|
|
131
|
-
),
|
|
132
|
-
balance_date: str | None = typer.Option(
|
|
133
|
-
None,
|
|
134
|
-
"--date",
|
|
135
|
-
"-d",
|
|
136
|
-
help="Date of the balance assertion (YYYY-MM-DD). Defaults to today.",
|
|
137
|
-
),
|
|
138
|
-
pad_date: str | None = typer.Option(
|
|
139
|
-
None,
|
|
140
|
-
"--pad-date",
|
|
141
|
-
help="Date of the pad directive (YYYY-MM-DD). Defaults to balance-date minus 1 day.",
|
|
142
|
-
),
|
|
143
|
-
json_data: str | None = typer.Option(
|
|
144
|
-
None, "--input", "-i", help="JSON string data (or '-' to read from STDIN)"
|
|
145
|
-
),
|
|
146
|
-
target: Path | None = typer.Option(None, "--target", help="Override target file to write to"),
|
|
147
|
-
):
|
|
148
|
-
"""Adjust an account balance using a Pad + Balance directive pair.
|
|
149
|
-
|
|
150
|
-
Beancount inserts a synthetic transaction to bring ACCOUNT to AMOUNT
|
|
151
|
-
on BALANCE-DATE. The adjustment is automatically booked to PAD-ACCOUNT.
|
|
152
|
-
|
|
153
|
-
Example:
|
|
154
|
-
uv run bean account pad-balance \\
|
|
155
|
-
--account Assets:BE:Wise:EUR \\
|
|
156
|
-
--amount 1777 --currency EUR \\
|
|
157
|
-
--pad-account Expenses:Other
|
|
158
|
-
|
|
159
|
-
JSON example (for agent pipelines):
|
|
160
|
-
echo '{"account": "Assets:BE:Wise:EUR", "amount": {"number": 1777, "currency": "EUR"}, \\
|
|
161
|
-
"pad_account": "Expenses:Other", "balance_date": "2026-06-02"}' \\
|
|
162
|
-
| uv run bean account pad-balance --input -
|
|
163
|
-
"""
|
|
164
|
-
actual_file = get_ledger_file(file)
|
|
165
|
-
service = AccountService(actual_file)
|
|
166
|
-
|
|
167
|
-
if json_data:
|
|
168
|
-
data_input = json.loads(read_json_input(json_data))
|
|
169
|
-
model = PadBalanceModel(**data_input)
|
|
170
|
-
else:
|
|
171
|
-
missing = [
|
|
172
|
-
f
|
|
173
|
-
for f, v in [
|
|
174
|
-
("--account", account),
|
|
175
|
-
("--amount", amount),
|
|
176
|
-
("--currency", currency),
|
|
177
|
-
("--pad-account", pad_account),
|
|
178
|
-
]
|
|
179
|
-
if not v
|
|
180
|
-
]
|
|
181
|
-
if missing:
|
|
182
|
-
console.print(
|
|
183
|
-
f"[red]Error: {', '.join(missing)} required when not using --input.[/red]"
|
|
184
|
-
)
|
|
185
|
-
sys.exit(typer.EXIT_VALIDATION)
|
|
186
|
-
|
|
187
|
-
b_date = date.fromisoformat(balance_date) if balance_date else date.today()
|
|
188
|
-
p_date = date.fromisoformat(pad_date) if pad_date else None
|
|
189
|
-
|
|
190
|
-
model = PadBalanceModel(
|
|
191
|
-
balance_date=b_date,
|
|
192
|
-
account=account, # type: ignore[arg-type]
|
|
193
|
-
amount={"number": amount, "currency": currency},
|
|
194
|
-
pad_account=pad_account, # type: ignore[arg-type]
|
|
195
|
-
pad_date=p_date,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
service.add_pad_balance(model, target_file=target)
|
|
199
|
-
console.print(
|
|
200
|
-
f"[green]Pad + Balance for {model.account} → {model.amount.number} "
|
|
201
|
-
f"{model.amount.currency} added.[/green]"
|
|
202
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|