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.
Files changed (34) hide show
  1. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/CHANGELOG.md +15 -0
  2. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/PKG-INFO +25 -2
  3. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/README.md +23 -0
  4. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/pyproject.toml +2 -2
  5. beancount_cli-0.2.16/src/beancount_cli/__init__.py +1 -0
  6. beancount_cli-0.2.16/src/beancount_cli/commands/account.py +178 -0
  7. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/commodity.py +14 -28
  8. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/common.py +0 -7
  9. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/transaction.py +21 -19
  10. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/models.py +6 -2
  11. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/services.py +0 -7
  12. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_cli.py +14 -7
  13. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_coverage_gap.py +12 -6
  14. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_models.py +7 -7
  15. beancount_cli-0.2.14/src/beancount_cli/__init__.py +0 -1
  16. beancount_cli-0.2.14/src/beancount_cli/commands/account.py +0 -202
  17. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/.gitignore +0 -0
  18. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/LICENSE +0 -0
  19. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/adapters.py +0 -0
  20. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/cli.py +0 -0
  21. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/__init__.py +0 -0
  22. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/price.py +0 -0
  23. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/report.py +0 -0
  24. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/commands/root.py +0 -0
  25. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/config.py +0 -0
  26. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/formatting.py +0 -0
  27. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/src/beancount_cli/py.typed +0 -0
  28. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/conftest.py +0 -0
  29. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/smoke_test.py +0 -0
  30. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_advanced.py +0 -0
  31. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_bql.py +0 -0
  32. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_config.py +0 -0
  33. {beancount_cli-0.2.14 → beancount_cli-0.2.16}/tests/test_price_cash_currency.py +0 -0
  34. {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.14
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.13
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.14"
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.13",
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 | None = typer.Argument(None, help="Currency code (e.g. USD)"),
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
- json_data: str | None = typer.Option(
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
- actual_file = get_ledger_file(file)
174
- service = CommodityService(actual_file)
175
-
176
- if json_data:
177
- data_input = json.loads(read_json_input(json_data))
178
- items = data_input if isinstance(data_input, list) else [data_input]
179
- for item in items:
180
- curr = item.get("currency")
181
- comm_name = item.get("name")
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, read_json_input
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
- json_data: str = typer.Option(
54
- ..., "--input", "-i", help="JSON string data (or '-' to read from STDIN)"
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
- actual_file = get_ledger_file(file)
62
- service = TransactionService(actual_file)
63
-
64
- data = json.loads(read_json_input(json_data))
65
-
66
- if isinstance(data, list):
67
- ta = TypeAdapter(list[TransactionModel])
68
- models = ta.validate_python(data)
69
- for m in models:
70
- service.add_transaction(m, draft=draft, print_only=print_only, target_file=target)
71
- else:
72
- model = TransactionModel(**data)
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(f"Invalid account name format: {v}")
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
- payload = {
57
- "date": "2023-12-01",
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", "add", "--file", str(temp_beancount_file), "--input", json.dumps(payload)
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 test_account_create_json_batch(temp_beancount_file):
45
- """Exercises the JSON batch path in account_create_cmd."""
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", "create", "--file", str(temp_beancount_file), "--input", payload
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 format"):
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 format"):
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