Tax-Ready Books Check (2025)
Is this client's 2025 file actually ready for the tax preparer? One click answers it: parked and uncategorized balances, duplicate entries, wrong-sign amounts, months that were never fully entered - and a plain-English punch list ending in a READY / NOT YET verdict. Built for the
9 steps · shared by Klajdi · September 3, 2026
No data is shared in this template. It contains only the recipe — column names and SQL logic. When you run it, your data is processed in your own browser and never leaves your machine.
What data it expects
QBO_GeneralLedger.csv
account_number · VARCHARaccount_type · VARCHARaccount · VARCHARdate · DATEname · VARCHARmemo · VARCHARamount · DOUBLE
QBO_ProfitAndLoss_Month.csv
section · VARCHARaccount · VARCHAR<one column per month, YYYY-MM> · DOUBLE
QBO_TrialBalance.csv
account_number · VARCHARaccount · VARCHARtype · VARCHARdebit · DOUBLEcredit · DOUBLE
Connect QuickBooks Online or drop a CSV / Excel export with a similar layout — the AI adapts the workflow if your columns differ.
How it works — every step, readable
- 01Load 2025 general ledger
SELECT * FROM read_csv_auto('QBO_GeneralLedger.csv', header=true) - 02Load 2025 P&L (account sections)
SELECT * FROM read_csv_auto('QBO_ProfitAndLoss_Month.csv', header=true) - 03Load 2025 trial balance
SELECT * FROM read_csv_auto('QBO_TrialBalance.csv', header=true) - 04Check 1 - Parked & uncategorized balancesaggregates rows into summary totals · filters to the relevant rows · sorts the output
SELECT account, COUNT(*) AS transactions, ROUND(SUM(amount), 2) AS total_amount, MIN(date) AS first_seen, MAX(date) AS last_seen FROM input_1 WHERE TRY_CAST(date AS DATE) IS NOT NULL AND ( account ILIKE '%uncategor%' OR account ILIKE '%ask my account%' OR account ILIKE '%suspense%' OR account ILIKE '%miscellan%' OR account ILIKE '%opening balance%') GROUP BY account ORDER BY ABS(SUM(amount)) DESC - 05Check 2 - Duplicate-looking entriesaggregates rows into summary totals · filters to the relevant rows · sorts the output
SELECT account, date, name AS payee, amount, COUNT(*) AS times_recorded, ROUND(SUM(amount), 2) AS combined_total FROM input_1 WHERE TRY_CAST(date AS DATE) IS NOT NULL AND amount <> 0 GROUP BY account, date, name, amount HAVING COUNT(*) > 1 ORDER BY ABS(SUM(amount)) DESC - 06Check 3 - Wrong-sign amountscombines data from multiple inputs · buckets values by condition · filters to the relevant rows
WITH sections AS (SELECT DISTINCT section, account FROM input_2) SELECT g.account, s.section, g.date, g.name AS payee, g.amount, CASE WHEN s.section ILIKE 'income%' THEN 'Negative amount on an income account - customer refund or misposting?' ELSE 'Negative amount on an expense account - vendor credit or misposting?' END AS why_flagged FROM input_1 g JOIN sections s ON s.account = g.account WHERE g.amount < 0 AND ( s.section ILIKE 'income%' OR s.section ILIKE 'expense%' OR s.section ILIKE 'cost of goods%' OR s.section ILIKE 'other expense%') ORDER BY g.amount ASC - 07Check 4 - Month-by-month coverageaggregates rows into summary totals · buckets values by condition · filters to the relevant rows
WITH m AS ( SELECT STRFTIME(TRY_CAST(date AS DATE), '%Y-%m') AS month, COUNT(*) AS transactions, ROUND(SUM(ABS(amount)), 0) AS total_activity FROM input_1 WHERE TRY_CAST(date AS DATE) IS NOT NULL GROUP BY 1), med AS (SELECT MEDIAN(transactions) AS typical FROM m) SELECT m.month, m.transactions, m.total_activity, CASE WHEN m.transactions < 0.25 * (SELECT typical FROM med) THEN 'LOW - was this month fully entered and reconciled?' ELSE 'OK' END AS coverage_check FROM m ORDER BY m.month - 08Check 5 - Trial balance ties to zerocomputes running / windowed totals · appends result sets (e.g. a TOTAL row) · sorts the output
SELECT account_number, account, type, debit, credit FROM ( SELECT 1 AS ord, ROW_NUMBER() OVER () AS rn, CAST(account_number AS VARCHAR) AS account_number, account, type, ROUND(COALESCE(debit, 0), 2) AS debit, ROUND(COALESCE(credit, 0), 2) AS credit FROM input_1 UNION ALL SELECT 2, 0, '', 'TOTAL', '', ROUND(SUM(COALESCE(debit, 0)), 2), ROUND(SUM(COALESCE(credit, 0)), 2) FROM input_1 UNION ALL SELECT 3, 0, '', 'DIFFERENCE (must be 0.00)', '', ROUND(SUM(COALESCE(debit, 0)) - SUM(COALESCE(credit, 0)), 2), 0.00 FROM input_1 ) t ORDER BY t.ord, t.rn - 09PUNCH LIST - fix these, then filebuckets values by condition · appends result sets (e.g. a TOTAL row) · filters to the relevant rows
SELECT ord, area, finding, next_step FROM ( SELECT 1 AS ord, 'COVERAGE' AS area, 'Books cover ' || (SELECT COUNT(*) FROM input_4) || ' of 12 months of 2025' AS finding, CASE WHEN (SELECT COUNT(*) FROM input_4) < 12 THEN 'Missing months block filing - enter and reconcile them first' ELSE 'All 12 months present' END AS next_step UNION ALL SELECT 2, 'PARKED BALANCES', (SELECT COUNT(*) FROM input_1) || ' uncategorized/suspense account(s) holding $' || COALESCE((SELECT ROUND(SUM(ABS(total_amount)), 0) FROM input_1), 0), CASE WHEN (SELECT COUNT(*) FROM input_1) > 0 THEN 'Recategorize every parked transaction before the preparer sees the books' ELSE 'Clean - nothing parked' END UNION ALL SELECT 3, 'DUPLICATES', (SELECT COUNT(*) FROM input_2) || ' duplicate-looking entries (same date, payee, amount)', CASE WHEN (SELECT COUNT(*) FROM input_2) > 0 THEN 'Review each - delete true duplicates, document the rest' ELSE 'None found' END UNION ALL SELECT 4, 'WRONG-SIGN AMOUNTS', (SELECT COUNT(*) FROM input_3) || ' negative amounts on income/expense accounts', CASE WHEN (SELECT COUNT(*) FROM input_3) > 0 THEN 'Confirm each is a real refund/credit - repost any mispostings' ELSE 'None found' END UNION ALL SELECT 5, 'UNDER-ENTERED MONTHS', (SELECT COUNT(*) FROM input_4 WHERE coverage_check LIKE 'LOW%') || ' month(s) with unusually low activity', CASE WHEN (SELECT COUNT(*) FROM input_4 WHERE coverage_check LIKE 'LOW%') > 0 THEN 'Check bank feeds and reconciliations for those months' ELSE 'Activity looks consistent' END UNION ALL SELECT 6, 'TRIAL BALANCE', 'Debits vs credits differ by $' || COALESCE((SELECT ABS(debit) FROM input_5 WHERE account LIKE 'DIFFERENCE%'), 0) || ' (YTD through Dec 2025)', CASE WHEN COALESCE((SELECT ABS(debit) FROM input_5 WHERE account LIKE 'DIFFERENCE%'), 0) > 0.005 THEN 'Books do not balance - resolve before anything else' ELSE 'Ties to zero - the handoff artifact is clean' END UNION ALL SELECT 7, 'VERDICT', CASE WHEN (SELECT COUNT(*) FROM input_1) + (SELECT COUNT(*) FROM input_2) + (SELECT COUNT(*) FROM input_3) + (SELECT COUNT(*) FROM input_4 WHERE coverage_check LIKE 'LOW%') + CASE WHEN (SELECT COUNT(*) FROM input_4) < 12 THEN 1 ELSE 0 END + CASE WHEN COALESCE((SELECT ABS(debit) FROM input_5 WHERE account LIKE 'DIFFERENCE%'), 0) > 0.005 THEN 1 ELSE 0 END = 0 THEN 'READY - these books look tax-ready. Export and hand off.' ELSE 'NOT YET - clear the items above before a clean handoff to the tax preparer.' END, 'Fix in QuickBooks, then rerun this check in one click' ) ORDER BY ord
Run this on your books
Free to try — no sign-up, no card. The workflow runs in your browser; your data never leaves your machine.
Make it your own →