diff options
Diffstat (limited to 'firefly_cli/commands/transaction.py')
| -rw-r--r-- | firefly_cli/commands/transaction.py | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py index 67d8b71..df1ffc4 100644 --- a/firefly_cli/commands/transaction.py +++ b/firefly_cli/commands/transaction.py @@ -1,5 +1,6 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only from firefly_cli import registry, output +from firefly_cli.errors import FireflyError # Inference table keyed by (source_type, destination_type) -> firefly tx type. def _infer_type(src_type, dst_type): @@ -29,6 +30,10 @@ def _add_args(p): p.add_argument("--tags", default=None, help="comma-separated") p.add_argument("--type", default=None, help="withdrawal|deposit|transfer (overrides inference)") + p.add_argument("--dry-run", dest="dry_run", action="store_true", + help="resolve accounts and show what would be sent; write nothing") + p.add_argument("--skip-dupes", dest="skip_dupes", action="store_true", + help="skip if a tx with same amount+date+source+destination exists") @registry.command("tx add", help="record a transaction; source/destination resolve to accounts, category/tags auto-create", args=_add_args) def cmd_add(args, ctx): @@ -49,16 +54,91 @@ def cmd_add(args, ctx): split["category_name"] = args.category if args.tags: split["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()] + if ttype == "transfer": + # Transfer direction is easy to reverse silently (ISSUES.md #5); echo it + # to stderr so the user/agent can catch a swapped --from/--to. stdout + # JSON unchanged; shown for both real writes and --dry-run. + import sys + print(f'transfer: {src["name"]} → {dst["name"]}, {split["amount"]}', + file=sys.stderr) + if args.dry_run: + # Accounts already resolved above (missing name = hard error). Write nothing. + # dry-run wins over skip-dupes: caller wants a preview, not a write. + output.emit({"dry_run": True, "would_send": split}, human=ctx.human) + return 0 + if args.skip_dupes: + # Firefly's search matches amount numerically; names quoted for spaces. + query = (f'amount_is:{split["amount"]} date_on:{split["date"]} ' + f'source_account_is:"{src["name"]}" ' + f'destination_account_is:"{dst["name"]}"') + hits = output.unwrap(ctx.client.request( + "GET", "/api/v1/search/transactions", params={"query": query})) + if hits: + output.emit({"skipped": "duplicate", "matched_id": hits[0].get("id")}, + human=ctx.human) + return 0 resp = ctx.client.request("POST", "/api/v1/transactions", body={"transactions": [split]}) output.emit(output.unwrap(resp), human=ctx.human) return 0 +def _edit_args(p): + p.add_argument("id") + p.add_argument("--amount", default=None) + p.add_argument("--date", default=None, help="YYYY-MM-DD") + p.add_argument("--desc", default=None) + p.add_argument("--from", dest="source", default=None, help="source account") + p.add_argument("--to", dest="dest", default=None, help="destination account") + p.add_argument("--category", default=None) + p.add_argument("--tags", default=None, help="comma-separated") + p.add_argument("--type", default=None, help="withdrawal|deposit|transfer") + +# ponytail: single-split journals only; multi-split edits need transaction_journal_id per row. +@registry.command("tx edit", help="modify one transaction by id; only the fields you pass change", args=_edit_args) +def cmd_edit(args, ctx): + split = {} + if args.amount is not None: + split["amount"] = str(args.amount) + if args.date is not None: + split["date"] = args.date + if args.desc is not None: + split["description"] = args.desc + if args.source is not None: + split["source_id"] = ctx.resolver.account(args.source)["id"] + if args.dest is not None: + split["destination_id"] = ctx.resolver.account(args.dest)["id"] + if args.category is not None: + split["category_name"] = args.category + if args.tags is not None: + split["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()] + if args.type is not None: + split["type"] = args.type + if not split: + raise FireflyError("tx edit: nothing to change; pass at least one field") + resp = ctx.client.request("PUT", f"/api/v1/transactions/{args.id}", + body={"transactions": [split]}) + output.emit(output.unwrap(resp), human=ctx.human) + return 0 + +def _delete_args(p): + p.add_argument("id") + p.add_argument("--yes", action="store_true", help="confirm deletion (required)") + +@registry.command("tx delete", help="delete one transaction by id (requires --yes)", args=_delete_args) +def cmd_delete(args, ctx): + if not args.yes: + raise FireflyError(f"tx delete {args.id}: refusing without --yes") + ctx.client.request("DELETE", f"/api/v1/transactions/{args.id}") + output.emit({"deleted": args.id}, human=ctx.human) + return 0 + def _list_args(p): p.add_argument("--since", default=None, help="start date YYYY-MM-DD") p.add_argument("--until", default=None, help="end date YYYY-MM-DD") p.add_argument("--account", default=None, help="filter by account name") p.add_argument("--limit", type=int, default=20) + p.add_argument("--all", action="store_true", + help="fetch every page (ignores --limit truncation)") @registry.command("tx list", help="list recent transactions (newest first)", args=_list_args) def cmd_list(args, ctx): @@ -72,7 +152,25 @@ def cmd_list(args, ctx): params["start"] = args.since if args.until: params["end"] = args.until + + if args.all: + rows, page = [], 1 + while True: + resp = ctx.client.request("GET", path, params={**params, "page": page}) + rows.extend(output.unwrap(resp)) + pg = (resp.get("meta") or {}).get("pagination") or {} + if page >= (pg.get("total_pages") or 1): + break + page += 1 + output.emit(rows, human=ctx.human) + return 0 + resp = ctx.client.request("GET", path, params=params) + pg = (resp.get("meta") or {}).get("pagination") or {} + total, count = pg.get("total"), pg.get("count") + if total is not None and count is not None and count < total: + import sys + print(f"showing {count} of {total} (use --all for all)", file=sys.stderr) output.emit(output.unwrap(resp), human=ctx.human) return 0 |
