From b078c5980facc0ebe4acd1f251f6ae3dad561292 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 1 Jul 2026 10:29:04 +0200 Subject: 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 --- SKILL.md | 4 +++- completions/firefly.bash | 1 + firefly_cli/__init__.py | 2 +- firefly_cli/commands/account.py | 15 ++++++++++++++- pyproject.toml | 2 +- tests/unit/test_commands_account.py | 16 +++++++++++++++- 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/SKILL.md b/SKILL.md index 98b9472..672c97e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 -firefly account balance +firefly account balance [--at YYYY-MM-DD] firefly account create --type asset|expense|revenue [--opening-balance N] [--currency CODE] firefly tx add --from --to @@ -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. # 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) -- cgit v1.2.3