1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
#!/bin/bash
# desktop-assistant tool: personal finance from Firefly III (read-only).
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# finance.sh WHAT [SINCE] [ACCOUNT]: WHAT is balances (active asset
# accounts), budgets (this month, spent against the limit) or spending
# (withdrawals from SINCE, YYYY-MM-DD, default the first of this month,
# optionally only from ACCOUNT: total and the 10 largest). Only firefly's list commands run here: nothing is recorded
# or edited.
set -uo pipefail
ff() { firefly "$@" || { echo "firefly $1 $2 failed" >&2; exit 1; }; }
case "${1:-}" in
balances)
ff account list --type asset | jq -r '.[] | select(.active)
| "\(.name): \(.current_balance | tonumber * 100 | round / 100) \(.currency_code)"'
;;
budgets)
ff budget list | jq -r '.[] | select(.active)
| "\(.name): spent \([.spent[].sum | tonumber] | add // 0 | fabs * 100 | round / 100)"
+ (if .auto_budget_amount then " of \(.auto_budget_amount | tonumber)" else "" end)
+ " \(.currency_code // "")"'
;;
spending)
since="${2:-$(date +%Y-%m-01)}"
date -d "$since" >/dev/null 2>&1 || { echo "since must be YYYY-MM-DD" >&2; exit 1; }
account=()
if [ -n "${3:-}" ]; then
# firefly's own "no such account" lists every expense account too;
# the model only needs the user's own accounts to retry.
names=$(ff account list --type asset | jq -r '.[] | select(.active) | .name')
grep -qixF -- "$3" <<<"$names" \
|| { echo "no account named $3; accounts: $(paste -sd, <<<"$names")" >&2; exit 1; }
# The per-account listing needs an end date, or the API rejects it.
account=(--account "$3" --until "$(date +%F)")
fi
json=$(firefly tx list --since "$since" "${account[@]}" --all --flat) || exit 1
jq -r --arg since "$since" --arg acct "${3:-all accounts}" '
[.[] | select(.type == "withdrawal") | .amount |= tonumber]
| "spent from \($acct) since \($since): \(map(.amount) | add // 0 | . * 100 | round / 100) in \(length) payments",
(sort_by(-.amount)[:10][]
| "\(.date[:10]) \(.amount * 100 | round / 100) \(.destination_name): \(.description)")' <<<"$json"
;;
*)
echo "what must be balances, budgets or spending" >&2
exit 1
;;
esac
|