sqlite-export-for-ynab 1.6.2__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlite_export_for_ynab-2.1.0/PKG-INFO +476 -0
- sqlite_export_for_ynab-2.1.0/README.md +456 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/pyproject.toml +12 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/setup.cfg +3 -3
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/_main.py +66 -62
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/ddl/create-relations.sql +77 -37
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/ddl/drop-relations.sql +1 -1
- sqlite_export_for_ynab-2.1.0/sqlite_export_for_ynab.egg-info/PKG-INFO +476 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/testing/fixtures.py +74 -10
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/tests/_main_test.py +133 -63
- sqlite_export_for_ynab-1.6.2/PKG-INFO +0 -206
- sqlite_export_for_ynab-1.6.2/README.md +0 -186
- sqlite_export_for_ynab-1.6.2/sqlite_export_for_ynab.egg-info/PKG-INFO +0 -206
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/LICENSE +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/setup.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/__main__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/ddl/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab/py.typed +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab.egg-info/SOURCES.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab.egg-info/dependency_links.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab.egg-info/entry_points.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab.egg-info/requires.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/sqlite_export_for_ynab.egg-info/top_level.txt +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/testing/__init__.py +0 -0
- {sqlite_export_for_ynab-1.6.2 → sqlite_export_for_ynab-2.1.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlite_export_for_ynab
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: SQLite Export for YNAB - Export YNAB Data to SQLite
|
|
5
|
+
Home-page: https://github.com/mxr/sqlite-export-for-ynab
|
|
6
|
+
Author: Max R
|
|
7
|
+
Author-email: maxr@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ynab,sqlite,sql,budget,plan,cli
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: aiohttp>=3
|
|
18
|
+
Requires-Dist: tldm
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# sqlite-export-for-ynab
|
|
22
|
+
|
|
23
|
+
[](https://results.pre-commit.ci/latest/github/mxr/sqlite-export-for-ynab/main) [](https://codecov.io/github/mxr/sqlite-export-for-ynab)
|
|
24
|
+
|
|
25
|
+
SQLite Export for YNAB - Export YNAB Budget Data to SQLite
|
|
26
|
+
|
|
27
|
+
## What This Does
|
|
28
|
+
|
|
29
|
+
Export all your [YNAB](https://ynab.com/) plans to a local [SQLite](https://www.sqlite.org/) DB. Then you can query your data with any tools compatible with SQLite.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ pip install sqlite-export-for-ynab
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### CLI
|
|
40
|
+
|
|
41
|
+
Provision a [YNAB Personal Access Token](https://api.ynab.com/#personal-access-tokens) and save it as an environment variable.
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
$ export YNAB_PERSONAL_ACCESS_TOKEN="..."
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run the tool from the terminal to download your plans:
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ sqlite-export-for-ynab
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Running it again will pull only data that changed since the last pull (this is done with [Delta Requests](https://api.ynab.com/#deltas)). If you want to wipe the DB and pull all data again use the `--full-refresh` flag.
|
|
54
|
+
|
|
55
|
+
<a id="db-path"></a>You can specify the DB path with the following options
|
|
56
|
+
1. The `--db` flag.
|
|
57
|
+
1. The `XDG_DATA_HOME` variable (see the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/index.html)). In that case the DB is saved in `"${XDG_DATA_HOME}"/sqlite-export-for-ynab/db.sqlite`.
|
|
58
|
+
1. If neither is set, the DB is saved in `~/.local/share/sqlite-export-for-ynab/db.sqlite`.
|
|
59
|
+
|
|
60
|
+
### Library
|
|
61
|
+
|
|
62
|
+
The library exposes the package `sqlite_export_for_ynab` and two functions - `default_db_path` and `sync`. You can use them as follows:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
import os
|
|
67
|
+
|
|
68
|
+
from sqlite_export_for_ynab import default_db_path
|
|
69
|
+
from sqlite_export_for_ynab import sync
|
|
70
|
+
|
|
71
|
+
db = default_db_path()
|
|
72
|
+
token = os.environ["YNAB_PERSONAL_ACCESS_TOKEN"]
|
|
73
|
+
full_refresh = False
|
|
74
|
+
|
|
75
|
+
asyncio.run(sync(token, db, full_refresh))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Relations
|
|
79
|
+
|
|
80
|
+
The relations are defined in [create-relations.sql](sqlite_export_for_ynab/ddl/create-relations.sql). They are 1:1 with [YNAB's OpenAPI Spec](https://api.ynab.com/papi/open_api_spec.yaml) (ex: transactions, accounts, etc) with some additions:
|
|
81
|
+
|
|
82
|
+
1. Some objects are pulled out into their own tables so they can be more cleanly modeled in SQLite (ex: subtransactions, loan account periodic values).
|
|
83
|
+
1. Foreign keys are added as needed (ex: plan ID, transaction ID) so data across plans remains separate.
|
|
84
|
+
1. Two new views called `flat_transactions` and `scheduled_flat_transactions`. These allow you to query split and non-split transactions easily, without needing to also query `subtransactions` and `scheduled_subtransactions` respectively. They also filter out deleted transactions/subtransactions and project payee/category fields to make querying more ergonomic.
|
|
85
|
+
|
|
86
|
+
## Querying
|
|
87
|
+
|
|
88
|
+
You can issue queries with typical SQLite tools. *`sqlite-export-for-ynab` deliberately does not implement a SQL REPL.*
|
|
89
|
+
|
|
90
|
+
### Sample Queries
|
|
91
|
+
|
|
92
|
+
You can run the queries from this README using a tool like [`mdq`](https://github.com/yshavit/mdq). For example:
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
$ mdq '```sql dupes' path/to/sqlite-export-for-ynab/README.md -o plain \
|
|
96
|
+
| sqlite3 path/to/sqlite-export-for-ynab/db.sqlite
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The DB path is documented [above](#db-path).
|
|
100
|
+
|
|
101
|
+
To get the top 5 payees by spending per plan, you could do:
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
WITH ranked_payees AS (
|
|
105
|
+
SELECT
|
|
106
|
+
pl.name AS plan_name
|
|
107
|
+
, t.payee_name AS payee
|
|
108
|
+
, SUM(t.amount_currency) AS net_spent
|
|
109
|
+
, ROW_NUMBER()
|
|
110
|
+
OVER (PARTITION BY pl.id ORDER BY SUM(t.amount) ASC)
|
|
111
|
+
AS rnk
|
|
112
|
+
FROM flat_transactions AS t INNER JOIN plans AS pl ON t.plan_id = pl.id
|
|
113
|
+
WHERE
|
|
114
|
+
t.payee_name != 'Starting Balance' AND t.transfer_account_id IS NULL
|
|
115
|
+
GROUP BY pl.id, t.payee_id
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
SELECT
|
|
119
|
+
plan_name
|
|
120
|
+
, payee
|
|
121
|
+
, net_spent
|
|
122
|
+
FROM ranked_payees
|
|
123
|
+
WHERE rnk <= 5
|
|
124
|
+
ORDER BY plan_name ASC, net_spent DESC
|
|
125
|
+
;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To get duplicate payees, or payees with no transactions:
|
|
129
|
+
|
|
130
|
+
```sql
|
|
131
|
+
WITH used_payees AS (
|
|
132
|
+
SELECT
|
|
133
|
+
plan_id
|
|
134
|
+
, payee_id
|
|
135
|
+
FROM transactions
|
|
136
|
+
WHERE
|
|
137
|
+
TRUE
|
|
138
|
+
AND payee_id IS NOT NULL
|
|
139
|
+
AND NOT deleted
|
|
140
|
+
UNION
|
|
141
|
+
SELECT
|
|
142
|
+
plan_id
|
|
143
|
+
, payee_id
|
|
144
|
+
FROM subtransactions
|
|
145
|
+
WHERE
|
|
146
|
+
TRUE
|
|
147
|
+
AND payee_id IS NOT NULL
|
|
148
|
+
AND NOT deleted
|
|
149
|
+
UNION
|
|
150
|
+
SELECT
|
|
151
|
+
plan_id
|
|
152
|
+
, payee_id
|
|
153
|
+
FROM scheduled_transactions
|
|
154
|
+
WHERE
|
|
155
|
+
TRUE
|
|
156
|
+
AND payee_id IS NOT NULL
|
|
157
|
+
AND NOT deleted
|
|
158
|
+
UNION
|
|
159
|
+
SELECT
|
|
160
|
+
plan_id
|
|
161
|
+
, payee_id
|
|
162
|
+
FROM scheduled_subtransactions
|
|
163
|
+
WHERE
|
|
164
|
+
TRUE
|
|
165
|
+
AND payee_id IS NOT NULL
|
|
166
|
+
AND NOT deleted
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
SELECT
|
|
170
|
+
pl.name AS "plan"
|
|
171
|
+
, dupes.name AS payee
|
|
172
|
+
FROM (
|
|
173
|
+
SELECT
|
|
174
|
+
p.plan_id
|
|
175
|
+
, p.name
|
|
176
|
+
FROM payees AS p
|
|
177
|
+
LEFT JOIN used_payees AS up ON p.plan_id = up.plan_id AND p.id = up.payee_id
|
|
178
|
+
WHERE
|
|
179
|
+
TRUE
|
|
180
|
+
AND up.payee_id IS NULL
|
|
181
|
+
AND p.transfer_account_id IS NULL
|
|
182
|
+
AND p.name != 'Reconciliation Balance Adjustment'
|
|
183
|
+
AND p.name != 'Manual Balance Adjustment'
|
|
184
|
+
AND NOT p.deleted
|
|
185
|
+
UNION
|
|
186
|
+
SELECT
|
|
187
|
+
plan_id
|
|
188
|
+
, name
|
|
189
|
+
FROM payees
|
|
190
|
+
WHERE NOT deleted
|
|
191
|
+
GROUP BY plan_id, name
|
|
192
|
+
HAVING COUNT(*) > 1
|
|
193
|
+
) AS dupes
|
|
194
|
+
INNER JOIN plans AS pl ON dupes.plan_id = pl.id
|
|
195
|
+
ORDER BY "plan", payee
|
|
196
|
+
;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
To count the spend for a category (ex: "Apps") between this month and the next 11 months (inclusive):
|
|
200
|
+
|
|
201
|
+
```sql
|
|
202
|
+
SELECT
|
|
203
|
+
plan_id
|
|
204
|
+
, SUM(amount_currency) AS amount_currency
|
|
205
|
+
FROM (
|
|
206
|
+
SELECT
|
|
207
|
+
plan_id
|
|
208
|
+
, amount_currency
|
|
209
|
+
FROM flat_transactions
|
|
210
|
+
WHERE
|
|
211
|
+
category_name = 'Apps'
|
|
212
|
+
AND SUBSTR("date", 1, 7) = SUBSTR(DATE(), 1, 7)
|
|
213
|
+
UNION ALL
|
|
214
|
+
SELECT
|
|
215
|
+
plan_id
|
|
216
|
+
, amount_currency * (
|
|
217
|
+
CASE
|
|
218
|
+
WHEN frequency = 'monthly' THEN 11
|
|
219
|
+
ELSE 1 -- assumes yearly
|
|
220
|
+
END
|
|
221
|
+
) AS amount_currency
|
|
222
|
+
FROM scheduled_flat_transactions
|
|
223
|
+
WHERE
|
|
224
|
+
category_name = 'Apps'
|
|
225
|
+
AND SUBSTR(date_next, 1, 7) < SUBSTR(DATE('now', '+1 year'), 1, 7)
|
|
226
|
+
)
|
|
227
|
+
;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To estimate taxable interest for a given year[^1]:
|
|
231
|
+
|
|
232
|
+
```sql
|
|
233
|
+
-- Parameters expected by this query:
|
|
234
|
+
-- @tax_rate
|
|
235
|
+
-- @year
|
|
236
|
+
-- @plan_id (optional, defaults to output for all plans)
|
|
237
|
+
-- @estimated_additional_interest (optional,
|
|
238
|
+
-- estimated interest not in YNAB such as investment income)
|
|
239
|
+
-- @interest_reporting_threshold (optional, defaults to the $10
|
|
240
|
+
-- common threshold, but confirm with actual documents)
|
|
241
|
+
-- @interest_payee_name (optional, defaults to Interest)
|
|
242
|
+
--
|
|
243
|
+
-- Example with only required params:
|
|
244
|
+
-- sqlite3 -header -box path/to/db.sqlite \
|
|
245
|
+
-- -cmd '.parameter init' \
|
|
246
|
+
-- -cmd ".parameter set @tax_rate 0.25" \
|
|
247
|
+
-- -cmd ".parameter set @year 2025" \
|
|
248
|
+
-- < query.sql
|
|
249
|
+
--
|
|
250
|
+
-- Example with all params:
|
|
251
|
+
-- -cmd ".parameter set @tax_rate 0.25" \
|
|
252
|
+
-- -cmd ".parameter set @year 2025" \
|
|
253
|
+
-- -cmd ".parameter set @estimated_additional_interest 250.00" \
|
|
254
|
+
-- -cmd ".parameter set @interest_reporting_threshold 10" \
|
|
255
|
+
-- -cmd ".parameter set @interest_payee_name Interest" \
|
|
256
|
+
-- -cmd ".parameter set @plan_id your-plan-id" \
|
|
257
|
+
-- < query.sql
|
|
258
|
+
|
|
259
|
+
WITH interest_by_account AS (
|
|
260
|
+
SELECT
|
|
261
|
+
plan_id
|
|
262
|
+
, account_name
|
|
263
|
+
, SUM(-amount_currency) AS total
|
|
264
|
+
FROM flat_transactions
|
|
265
|
+
WHERE
|
|
266
|
+
TRUE
|
|
267
|
+
AND payee_name = COALESCE(NULLIF(@interest_payee_name, ''), 'Interest')
|
|
268
|
+
AND SUBSTR("date", 1, 4) = CAST(@year AS TEXT)
|
|
269
|
+
AND (COALESCE(@plan_id, '') = '' OR plan_id = @plan_id)
|
|
270
|
+
GROUP BY plan_id, account_name
|
|
271
|
+
HAVING total >= CAST(COALESCE(@interest_reporting_threshold, 10) AS REAL)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
, interest_by_plan AS (
|
|
275
|
+
SELECT
|
|
276
|
+
plans.id AS plan_id
|
|
277
|
+
, plans.name AS plan_name
|
|
278
|
+
, COALESCE(SUM(interest_by_account.total), 0) AS interest_in_ynab
|
|
279
|
+
FROM plans
|
|
280
|
+
LEFT JOIN interest_by_account ON plans.id = interest_by_account.plan_id
|
|
281
|
+
WHERE COALESCE(@plan_id, '') = '' OR plans.id = @plan_id
|
|
282
|
+
GROUP BY plan_id, plan_name
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
, ranked_interest AS (
|
|
286
|
+
SELECT
|
|
287
|
+
plan_id
|
|
288
|
+
, plan_name
|
|
289
|
+
, interest_in_ynab
|
|
290
|
+
, interest_in_ynab
|
|
291
|
+
+ CAST(COALESCE(@estimated_additional_interest, 0) AS REAL)
|
|
292
|
+
AS interest_with_estimate
|
|
293
|
+
, ROW_NUMBER() OVER (ORDER BY plan_name, plan_id) AS row_num
|
|
294
|
+
FROM interest_by_plan
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
, estimated_interest AS (
|
|
298
|
+
SELECT
|
|
299
|
+
plan_id
|
|
300
|
+
, plan_name
|
|
301
|
+
, interest_in_ynab
|
|
302
|
+
-- Additional interest is per-tax-return not per-YNAB-plan. Only add
|
|
303
|
+
-- additional interest to one plan's output to avoid double counting.
|
|
304
|
+
, CASE
|
|
305
|
+
WHEN row_num != 1 THEN interest_in_ynab
|
|
306
|
+
WHEN
|
|
307
|
+
interest_with_estimate
|
|
308
|
+
< CAST(COALESCE(@interest_reporting_threshold, 10) AS REAL)
|
|
309
|
+
THEN 0
|
|
310
|
+
ELSE interest_with_estimate
|
|
311
|
+
END AS estimated_total_taxable_interest
|
|
312
|
+
FROM ranked_interest
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
SELECT
|
|
316
|
+
plan_name AS "plan"
|
|
317
|
+
, PRINTF('%.2f', interest_in_ynab) AS interest_in_ynab
|
|
318
|
+
, PRINTF('%.2f', estimated_total_taxable_interest)
|
|
319
|
+
AS estimated_total_taxable_interest
|
|
320
|
+
, PRINTF(
|
|
321
|
+
'%.2f'
|
|
322
|
+
, estimated_total_taxable_interest * CAST(NULLIF(@tax_rate, '') AS REAL)
|
|
323
|
+
) AS estimated_tax_liability
|
|
324
|
+
FROM estimated_interest
|
|
325
|
+
ORDER BY plan_name, plan_id
|
|
326
|
+
;
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
To compare assigned category values to a given account's balance:
|
|
330
|
+
|
|
331
|
+
```sql
|
|
332
|
+
-- Parameters expected by this query:
|
|
333
|
+
-- @account_name_like (required, the account name to match against)
|
|
334
|
+
-- @plan_id (optional, defaults to output for all matching plans)
|
|
335
|
+
-- @include_category_groups
|
|
336
|
+
-- (optional, comma-separated category-group names to include;
|
|
337
|
+
-- exclusive with @exclude_category_groups)
|
|
338
|
+
-- @exclude_category_groups
|
|
339
|
+
-- (optional, comma-separated category-group names to exclude;
|
|
340
|
+
-- exclusive with @include_category_groups)
|
|
341
|
+
--
|
|
342
|
+
-- Example:
|
|
343
|
+
-- sqlite -header -box path/to/db.sqlite \
|
|
344
|
+
-- -cmd '.parameter init' \
|
|
345
|
+
-- -cmd ".parameter set @account_name_like %Savings%" \
|
|
346
|
+
-- -cmd ".parameter set @include_category_groups 'Home,Food'" \
|
|
347
|
+
-- < query.sql
|
|
348
|
+
WITH params AS (
|
|
349
|
+
SELECT
|
|
350
|
+
TRIM(COALESCE(@account_name_like, '')) AS account_name_like
|
|
351
|
+
, TRIM(COALESCE(@include_category_groups, ''))
|
|
352
|
+
AS include_category_groups
|
|
353
|
+
, TRIM(COALESCE(@exclude_category_groups, ''))
|
|
354
|
+
AS exclude_category_groups
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
, validation AS (
|
|
358
|
+
SELECT 'Set @account_name_like' AS error
|
|
359
|
+
FROM params AS p
|
|
360
|
+
WHERE p.account_name_like = ''
|
|
361
|
+
UNION ALL
|
|
362
|
+
SELECT
|
|
363
|
+
'Set only one of @include_category_groups'
|
|
364
|
+
|| ' or @exclude_category_groups' AS error
|
|
365
|
+
FROM params AS p
|
|
366
|
+
WHERE p.include_category_groups != '' AND p.exclude_category_groups != ''
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
SELECT v.error AS error_message
|
|
370
|
+
FROM validation AS v
|
|
371
|
+
WHERE v.error IS NOT NULL
|
|
372
|
+
;
|
|
373
|
+
|
|
374
|
+
WITH params AS (
|
|
375
|
+
SELECT
|
|
376
|
+
TRIM(COALESCE(@account_name_like, '')) AS account_name_like
|
|
377
|
+
, TRIM(COALESCE(@include_category_groups, ''))
|
|
378
|
+
AS include_category_groups
|
|
379
|
+
, TRIM(COALESCE(@exclude_category_groups, ''))
|
|
380
|
+
AS exclude_category_groups
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
, validation AS (
|
|
384
|
+
SELECT
|
|
385
|
+
p.account_name_like
|
|
386
|
+
, p.include_category_groups
|
|
387
|
+
, p.exclude_category_groups
|
|
388
|
+
FROM params AS p
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
, validation_errors AS (
|
|
392
|
+
SELECT 'Set @account_name_like' AS error
|
|
393
|
+
FROM validation AS v
|
|
394
|
+
WHERE v.account_name_like = ''
|
|
395
|
+
UNION ALL
|
|
396
|
+
SELECT
|
|
397
|
+
'Set only one of @include_category_groups'
|
|
398
|
+
|| ' or @exclude_category_groups' AS error
|
|
399
|
+
FROM validation AS v
|
|
400
|
+
WHERE v.include_category_groups != '' AND v.exclude_category_groups != ''
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
, valid_params AS (
|
|
404
|
+
SELECT
|
|
405
|
+
v.account_name_like
|
|
406
|
+
, v.include_category_groups
|
|
407
|
+
, v.exclude_category_groups
|
|
408
|
+
FROM validation AS v
|
|
409
|
+
WHERE NOT EXISTS (
|
|
410
|
+
SELECT 1
|
|
411
|
+
FROM validation_errors
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
, matched_accounts AS (
|
|
416
|
+
SELECT
|
|
417
|
+
p.id AS plan_id
|
|
418
|
+
, p.name AS plan_name
|
|
419
|
+
, a.name AS account_name
|
|
420
|
+
, a.cleared_balance / 1000.0 AS account_amount
|
|
421
|
+
FROM plans AS p
|
|
422
|
+
INNER JOIN accounts AS a ON p.id = a.plan_id
|
|
423
|
+
CROSS JOIN valid_params AS v
|
|
424
|
+
WHERE
|
|
425
|
+
TRUE
|
|
426
|
+
AND NOT a.deleted
|
|
427
|
+
AND a.name LIKE v.account_name_like
|
|
428
|
+
AND (COALESCE(@plan_id, '') = '' OR p.id = @plan_id)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
, category_totals AS (
|
|
432
|
+
SELECT
|
|
433
|
+
c.plan_id
|
|
434
|
+
, COALESCE(SUM(c.balance), 0) / 1000.0 AS total
|
|
435
|
+
FROM categories AS c CROSS JOIN valid_params AS v
|
|
436
|
+
WHERE
|
|
437
|
+
TRUE
|
|
438
|
+
AND NOT c.deleted
|
|
439
|
+
AND c.category_group_name != 'Credit Card Payments'
|
|
440
|
+
AND c.category_group_name != 'Internal Master Category'
|
|
441
|
+
AND (
|
|
442
|
+
v.include_category_groups = ''
|
|
443
|
+
OR INSTR(
|
|
444
|
+
','
|
|
445
|
+
|| LOWER(REPLACE(v.include_category_groups, ', ', ','))
|
|
446
|
+
|| ','
|
|
447
|
+
, ',' || LOWER(c.category_group_name) || ','
|
|
448
|
+
)
|
|
449
|
+
> 0
|
|
450
|
+
)
|
|
451
|
+
AND (
|
|
452
|
+
v.exclude_category_groups = ''
|
|
453
|
+
OR INSTR(
|
|
454
|
+
','
|
|
455
|
+
|| LOWER(REPLACE(v.exclude_category_groups, ', ', ','))
|
|
456
|
+
|| ','
|
|
457
|
+
, ',' || LOWER(c.category_group_name) || ','
|
|
458
|
+
)
|
|
459
|
+
= 0
|
|
460
|
+
)
|
|
461
|
+
AND (COALESCE(@plan_id, '') = '' OR c.plan_id = @plan_id)
|
|
462
|
+
GROUP BY c.plan_id
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
SELECT
|
|
466
|
+
ma.plan_name AS "plan"
|
|
467
|
+
, ma.account_name AS account
|
|
468
|
+
, PRINTF('%.2f', COALESCE(ct.total, 0)) AS total
|
|
469
|
+
, PRINTF('%.2f', COALESCE(ct.total, 0) - ma.account_amount) AS excess
|
|
470
|
+
FROM matched_accounts AS ma
|
|
471
|
+
LEFT JOIN category_totals AS ct ON ma.plan_id = ct.plan_id
|
|
472
|
+
ORDER BY "plan", account
|
|
473
|
+
;
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
[^1]: This query is a rough estimate based on YNAB data and optional user inputs. It is not financial advice, tax advice, or a substitute for Forms 1099-INT, brokerage statements, bank records, or guidance from a qualified professional.
|