diff options
| author | Danilo M. <danix@danix.xyz> | 2026-07-01 10:29:04 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-07-01 10:29:04 +0200 |
| commit | b078c5980facc0ebe4acd1f251f6ae3dad561292 (patch) | |
| tree | 29611c1d9f039e96db81b4b2bc31b8d7729f0a36 | |
| parent | 0c9939c5961a3cef50e6378c0eefbeaae00f4c67 (diff) | |
| download | firefly-cli-b078c5980facc0ebe4acd1f251f6ae3dad561292.tar.gz firefly-cli-b078c5980facc0ebe4acd1f251f6ae3dad561292.zip | |
feat: account balance --at for historical balance (v0.3.2)
Reconciliation needed an account's balance as of a past date; the command
only reported the current balance, forcing hand-written sum scripts.
--at YYYY-MM-DD re-fetches the account with Firefly's ?date= param, which
recomputes current_balance as of that date (end of day, no client math).
Without --at the current-balance path is unchanged (no extra API call).
Verified live read-only against real data: Mediolanum current 183.23 vs
--at 2026-06-10 164.38, boundary probe gives distinct daily balances.
PATCH: new optional flag, contract unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | SKILL.md | 4 | ||||
| -rw-r--r-- | completions/firefly.bash | 1 | ||||
| -rw-r--r-- | firefly_cli/__init__.py | 2 | ||||
| -rw-r--r-- | firefly_cli/commands/account.py | 15 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rw-r--r-- | tests/unit/test_commands_account.py | 16 |
6 files changed, 35 insertions, 5 deletions
@@ -52,7 +52,7 @@ If `firefly` is not on PATH, run from the repo with `python -m firefly_cli ...` firefly auth test verify connectivity and token firefly account list [--type asset|expense|revenue|liability|...] firefly account get <name|id> -firefly account balance <name|id> +firefly account balance <name|id> [--at YYYY-MM-DD] firefly account create <name> --type asset|expense|revenue [--opening-balance N] [--currency CODE] firefly tx add <amount> --from <acct> --to <acct> @@ -105,6 +105,8 @@ automatically. Unlike categories/tags, accounts are NOT auto-created by **Check a balance:** ```bash firefly account balance test01 # -> {"id","name","current_balance"} +firefly account balance test01 --at 2026-05-31 # historical: balance as of that date + # -> adds "date"; useful for reconciliation ``` **Find recent spending in a window:** diff --git a/completions/firefly.bash b/completions/firefly.bash index 697cd1a..b23f081 100644 --- a/completions/firefly.bash +++ b/completions/firefly.bash @@ -37,6 +37,7 @@ _firefly() { local leaf_opts="" case "$group $leaf" in "auth set") leaf_opts="--token --url";; + "account balance") leaf_opts="--at";; "account create") leaf_opts="--currency --opening-balance --type";; "account list") leaf_opts="--type";; "tx add") leaf_opts="--category --date --desc --from --tags --to --type";; diff --git a/firefly_cli/__init__.py b/firefly_cli/__init__.py index 79ea6e3..eb73e59 100644 --- a/firefly_cli/__init__.py +++ b/firefly_cli/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> # Licensed under the GNU General Public License v2.0 only. -__version__ = "0.3.1" +__version__ = "0.3.2" diff --git a/firefly_cli/commands/account.py b/firefly_cli/commands/account.py index 9dbfab6..d78f8fc 100644 --- a/firefly_cli/commands/account.py +++ b/firefly_cli/commands/account.py @@ -52,9 +52,22 @@ def cmd_get(args, ctx): output.emit(acc, human=ctx.human) return 0 -@registry.command("account balance", help="show current balance for one account (name or id)", args=_name_arg) +def _balance_args(p): + p.add_argument("account", help="account name or id") + p.add_argument("--at", default=None, + help="balance as of this date YYYY-MM-DD (default: current)") + +@registry.command("account balance", help="show balance for one account (name or id); --at for a historical date", args=_balance_args) def cmd_balance(args, ctx): acc = ctx.resolver.account(args.account) + if args.at: + # Firefly recomputes current_balance as of ?date= on the account show endpoint. + dated = output.unwrap(ctx.client.request( + "GET", f"/api/v1/accounts/{acc['id']}", params={"date": args.at})) + output.emit({"id": acc["id"], "name": dated.get("name") or acc.get("name"), + "date": args.at, + "current_balance": dated.get("current_balance")}, human=ctx.human) + return 0 output.emit({"id": acc["id"], "name": acc.get("name"), "current_balance": acc.get("current_balance")}, human=ctx.human) return 0 diff --git a/pyproject.toml b/pyproject.toml index cb0fdaa..fd0d369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "firefly-iii-agent" -version = "0.3.1" +version = "0.3.2" description = "CLI tool for agent interaction with Firefly III" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/unit/test_commands_account.py b/tests/unit/test_commands_account.py index 2c07780..4a01292 100644 --- a/tests/unit/test_commands_account.py +++ b/tests/unit/test_commands_account.py @@ -22,11 +22,25 @@ class TestAccountCmd(unittest.TestCase): ctx, client, resolver = make_ctx() resolver.account.return_value = {"id": "3", "name": "Checking", "current_balance": "100.00"} - args = MagicMock(account="Checking") + args = MagicMock(account="Checking", at=None) rc = acct.cmd_balance(args, ctx) resolver.account.assert_called_once_with("Checking") + client.request.assert_not_called() # current balance from resolver, no extra call self.assertEqual(rc, 0) + def test_balance_at_date_fetches_dated_account(self): + ctx, client, resolver = make_ctx() + resolver.account.return_value = {"id": "3", "name": "Checking", + "current_balance": "100.00"} + client.request.return_value = {"data": {"id": "3", "attributes": { + "name": "Checking", "current_balance": "42.00"}}} + args = MagicMock(account="Checking", at="2026-05-31") + rc = acct.cmd_balance(args, ctx) + self.assertEqual(rc, 0) + method, path = client.request.call_args[0][:2] + self.assertEqual((method, path), ("GET", "/api/v1/accounts/3")) + self.assertEqual(client.request.call_args[1]["params"], {"date": "2026-05-31"}) + class TestAccountCreate(unittest.TestCase): def _args(self, **kw): base = dict(name=None, type=None, opening_balance=None, currency=None) |
