summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md6
-rw-r--r--README.md10
-rw-r--r--SKILL.md12
-rw-r--r--firefly_cli/commands/account.py32
-rw-r--r--pyproject.toml2
-rw-r--r--tests/unit/test_commands_account.py50
6 files changed, 106 insertions, 6 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index d98a427..15acecb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -30,8 +30,10 @@ No other files change.
## Conventions
- Python 3.11+, standard library only. No third-party runtime deps.
- Output JSON by default (agent-first); `--human` for tables.
-- Name args resolve to IDs; ambiguous or missing names are HARD errors listing
- candidates. Never silently guess an account (real money).
+- Account name args resolve to IDs; ambiguous or missing names are HARD errors
+ listing candidates. Never silently guess an account (real money).
+- Categories and tags are NOT resolved: their names pass straight to Firefly,
+ which auto-creates them. Accounts are never auto-created (`account create`).
- All errors are `firefly_cli.errors.FireflyError` subclasses; `cli.main`
catches them, prints `{"error": ...}` to stderr, returns exit code 1.
diff --git a/README.md b/README.md
index 36af8c7..c63bad9 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,8 @@ firefly auth test verify connectivity and token
firefly account list [--type T] list accounts (filter: asset, expense, ...)
firefly account get <name|id> show one account
firefly account balance <name> show an account balance
+firefly account create <name> --type asset|expense|revenue
+ [--opening-balance N] [--currency CODE]
firefly tx add <amount> --from <acct> --to <acct> [--desc T]
[--date YYYY-MM-DD] [--category C] [--tags a,b] [--type T]
@@ -49,9 +51,11 @@ The command set grows over time; see `CLAUDE.md` for how to add one.
- Output is JSON by default. Pass `--human` for aligned tables.
- Exit code is 0 on success, 1 on any error; errors print as
`{"error": "..."}` on stderr.
-- Account, category, and tag arguments accept names, which are resolved to IDs.
- An ambiguous or unknown name is a hard error listing the candidates, never a
- silent guess.
+- Account arguments accept names, which are resolved to IDs. An ambiguous or
+ unknown account is a hard error listing the candidates, never a silent guess.
+- Categories and tags are not resolved: `tx add --category`/`--tags` pass the
+ names straight to Firefly, which creates them if new. Accounts are never
+ auto-created; use `account create`.
- `tx add` infers the transaction type from the account types (asset to expense
is a withdrawal, revenue to asset is a deposit, asset to asset is a transfer).
Override with `--type`.
diff --git a/SKILL.md b/SKILL.md
index 373e5f4..7956bdd 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -53,6 +53,8 @@ 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 create <name> --type asset|expense|revenue
+ [--opening-balance N] [--currency CODE]
firefly tx add <amount> --from <acct> --to <acct>
[--desc TEXT] [--date YYYY-MM-DD] [--category NAME] [--tags a,b] [--type T]
firefly tx list [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--account NAME] [--limit N]
@@ -86,6 +88,16 @@ firefly tx add 1800 --from Salary --to test01 --desc "June pay"
firefly tx add 200 --from test01 --to Savings --type transfer
```
+**Create an account** (when `tx add` errors that an account does not exist,
+and the user confirms it should be created):
+```bash
+firefly account create Savings --type asset --opening-balance 0
+firefly account create Rent --type expense
+```
+Supports asset, expense, revenue. asset accounts get the default role
+automatically. Unlike categories/tags, accounts are NOT auto-created by
+`tx add`, create them explicitly here first.
+
**Check a balance:**
```bash
firefly account balance test01 # -> {"id","name","current_balance"}
diff --git a/firefly_cli/commands/account.py b/firefly_cli/commands/account.py
index fa2dc65..0e86b2b 100644
--- a/firefly_cli/commands/account.py
+++ b/firefly_cli/commands/account.py
@@ -1,5 +1,10 @@
# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
from firefly_cli import registry, output
+from firefly_cli.errors import FireflyError
+
+# v1 scope: the everyday types. Liabilities need extra required fields
+# (liability_type/direction/amount); add when needed.
+_CREATE_TYPES = ("asset", "expense", "revenue")
def _list_args(p):
p.add_argument("--type", help="filter: asset, expense, revenue, liability, ...")
@@ -11,6 +16,33 @@ def cmd_list(args, ctx):
output.emit(output.unwrap(resp), human=ctx.human)
return 0
+def _create_args(p):
+ p.add_argument("name", help="account name (must be unique)")
+ p.add_argument("--type", required=True,
+ help="asset, expense, or revenue")
+ p.add_argument("--opening-balance", dest="opening_balance", default=None,
+ help="initial balance (asset accounts); dated today")
+ p.add_argument("--currency", default=None, help="currency code, e.g. EUR")
+
+@registry.command("account create", help="create an account", args=_create_args)
+def cmd_create(args, ctx):
+ if args.type not in _CREATE_TYPES:
+ raise FireflyError(
+ f'Unsupported account type "{args.type}". '
+ f'Use one of: {", ".join(_CREATE_TYPES)}.')
+ body = {"name": args.name, "type": args.type}
+ if args.type == "asset":
+ body["account_role"] = "defaultAsset"
+ if args.opening_balance is not None:
+ from datetime import date as _date
+ body["opening_balance"] = str(args.opening_balance)
+ body["opening_balance_date"] = _date.today().isoformat()
+ if args.currency:
+ body["currency_code"] = args.currency
+ resp = ctx.client.request("POST", "/api/v1/accounts", body=body)
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0
+
def _name_arg(p):
p.add_argument("account", help="account name or id")
diff --git a/pyproject.toml b/pyproject.toml
index 5942795..99cf7d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "firefly-iii-agent"
-version = "0.1.0"
+version = "0.2.0"
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 4d28802..2c07780 100644
--- a/tests/unit/test_commands_account.py
+++ b/tests/unit/test_commands_account.py
@@ -2,6 +2,7 @@ import unittest
from unittest.mock import MagicMock
from firefly_cli.commands import account as acct
from firefly_cli.context import Context
+from firefly_cli.errors import FireflyError
def make_ctx():
client = MagicMock()
@@ -25,3 +26,52 @@ class TestAccountCmd(unittest.TestCase):
rc = acct.cmd_balance(args, ctx)
resolver.account.assert_called_once_with("Checking")
self.assertEqual(rc, 0)
+
+class TestAccountCreate(unittest.TestCase):
+ def _args(self, **kw):
+ base = dict(name=None, type=None, opening_balance=None, currency=None)
+ base.update(kw)
+ m = MagicMock()
+ m.configure_mock(**base) # 'name' is reserved in MagicMock ctor, not configure_mock
+ return m
+
+ def test_asset_posts_with_default_role(self):
+ ctx, client, _ = make_ctx()
+ client.request.return_value = {"data": {"id": "9", "attributes": {}}}
+ rc = acct.cmd_create(self._args(name="Savings", type="asset"), ctx)
+ self.assertEqual(rc, 0)
+ method, path = client.request.call_args[0][:2]
+ body = client.request.call_args[1]["body"]
+ self.assertEqual((method, path), ("POST", "/api/v1/accounts"))
+ self.assertEqual(body["name"], "Savings")
+ self.assertEqual(body["type"], "asset")
+ self.assertEqual(body["account_role"], "defaultAsset")
+
+ def test_expense_has_no_role(self):
+ ctx, client, _ = make_ctx()
+ client.request.return_value = {"data": {"id": "9", "attributes": {}}}
+ acct.cmd_create(self._args(name="Rent", type="expense"), ctx)
+ body = client.request.call_args[1]["body"]
+ self.assertNotIn("account_role", body)
+
+ def test_opening_balance_adds_date(self):
+ ctx, client, _ = make_ctx()
+ client.request.return_value = {"data": {"id": "9", "attributes": {}}}
+ acct.cmd_create(
+ self._args(name="Savings", type="asset", opening_balance="500"), ctx)
+ body = client.request.call_args[1]["body"]
+ self.assertEqual(body["opening_balance"], "500")
+ self.assertIn("opening_balance_date", body) # required_with by Firefly
+
+ def test_currency_passed_through(self):
+ ctx, client, _ = make_ctx()
+ client.request.return_value = {"data": {"id": "9", "attributes": {}}}
+ acct.cmd_create(
+ self._args(name="Savings", type="asset", currency="EUR"), ctx)
+ self.assertEqual(client.request.call_args[1]["body"]["currency_code"], "EUR")
+
+ def test_bad_type_is_hard_error_no_request(self):
+ ctx, client, _ = make_ctx()
+ with self.assertRaises(FireflyError):
+ acct.cmd_create(self._args(name="X", type="bogus"), ctx)
+ client.request.assert_not_called()