From 2db11aa5d34766e4d23ccc308c57c470b6aa6dba Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 30 Jun 2026 12:53:01 +0200 Subject: tx add: auto-create categories instead of requiring them Pass --category straight to Firefly III as category_name; the API creates the category if it does not exist, matching --tags behavior. Drops the resolver lookup that turned an unknown category into a hard error. Accounts still resolve strictly (real money). Co-Authored-By: Claude Opus 4.8 --- SKILL.md | 19 ++++++++++++------- firefly_cli/commands/transaction.py | 3 ++- tests/unit/test_commands_transaction.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/SKILL.md b/SKILL.md index 6051321..373e5f4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -32,11 +32,15 @@ If `firefly` is not on PATH, run from the repo with `python -m firefly_cli ...` object. Do not pass `--human` when you intend to parse, it prints tables. - **Exit code 0 = success, 1 = error.** On error, `{"error": "..."}` goes to stderr. Always check the exit code before trusting output. -- **You pass names, not IDs.** `--from test01`, `--category Groceries`. The CLI - resolves them. An unknown or ambiguous name is a HARD error that lists the - candidates and exits 1. When that happens, read the candidates, pick the - right one, and retry. NEVER guess an account, a wrong account moves real - money. +- **You pass names, not IDs.** `--from test01`, `--to Groceries`. For + **accounts** the CLI resolves the name to an ID; an unknown or ambiguous + account is a HARD error that lists the candidates and exits 1. When that + happens, read the candidates, pick the right one, and retry. NEVER guess an + account, a wrong account moves real money. +- **Categories and tags auto-create.** `--category NAME` and `--tags a,b` are + passed straight to Firefly, which creates the category/tag if it does not + exist. No resolution, no error on a new name. Reuse an existing name (see + `firefly category list` / `firefly tag list`) to avoid duplicates. - **`tx add` infers the transaction type** from the account types: asset to expense = withdrawal, revenue to asset = deposit, asset to asset = transfer. Override with `--type withdrawal|deposit|transfer` only when inference is @@ -68,8 +72,9 @@ Global: `--human` (tables, do not use when parsing), `--url`/`--token` firefly tx add 42.50 --from test01 --to Groceries --desc "weekly shop" --tags food ``` `test01` is an asset account, `Groceries` an expense account, so this is a -withdrawal. If `Groceries` does not exist, the CLI errors with the available -expense accounts; pick one or ask the user before creating a new category. +withdrawal. `Groceries` here is the destination **expense account** and must +exist (resolved by name). The `--tags food` and any `--category NAME` are +auto-created by Firefly if new. **Record income:** ```bash diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py index 7d48f78..be8d281 100644 --- a/firefly_cli/commands/transaction.py +++ b/firefly_cli/commands/transaction.py @@ -45,7 +45,8 @@ def cmd_add(args, ctx): "destination_id": dst["id"], } if args.category: - split["category_name"] = ctx.resolver.category(args.category).get("name", args.category) + # Pass name raw; Firefly auto-creates the category if it doesn't exist. + split["category_name"] = args.category if args.tags: split["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()] resp = ctx.client.request("POST", "/api/v1/transactions", diff --git a/tests/unit/test_commands_transaction.py b/tests/unit/test_commands_transaction.py index db187b7..7301002 100644 --- a/tests/unit/test_commands_transaction.py +++ b/tests/unit/test_commands_transaction.py @@ -55,6 +55,18 @@ class TestTxAdd(unittest.TestCase): self.assertEqual(split["type"], "transfer") self.assertEqual(split["tags"], ["food", "fun"]) + def test_category_passed_raw_not_resolved(self): + # Category name goes straight to Firefly (auto-creates); resolver untouched. + ctx, client, resolver = make_ctx() + resolver.account.side_effect = lambda n: {"id": "1", "type": "asset", "name": n} + client.request.return_value = {"data": {"id": "1", "attributes": {}}} + args = MagicMock(amount="5", source="A", dest="B", desc=None, date=None, + category="Brand New Cat", tags=None, type="withdrawal") + tx.cmd_add(args, ctx) + split = client.request.call_args[1]["body"]["transactions"][0] + self.assertEqual(split["category_name"], "Brand New Cat") + resolver.category.assert_not_called() + class TestTxList(unittest.TestCase): def test_list_passes_date_params(self): ctx, client, _ = make_ctx() -- cgit v1.2.3