#!/usr/bin/env python # ledgerstatus.py [options] [ledgerfile] ... # Report useful status information from one or more ledgers. Requires beancount. # Simon Michael 2008 # # [2:00pm] also I'd like something that shows me how much I am on top of financial tracking - how current my numbers are, when last reconciled etc - at a glance # [2:06pm] how could I measure that ? # [2:06pm] number of days since last ledger entry.. # [2:06pm] number of ledger entries in last 30 days (compared to average) # [2:07pm] number of days since last cleared checking entry (indicating an online reconcile) # [2:08pm] those would be a good start. How do I make those visual # [2:09pm] well I guess the first step is a script to print them import sys, os, re, time, optparse from datetime import date, timedelta from pprint import pprint as pp sys.path.append('/Users/simon/src/ledger/beancount/lib/python') sys.path.append('/Users/simon/src/ledger/beancount/lib/python-fallback') import beancount.cmdline # process options def usage(): # -> string print 'Usage: %s' % sys.argv[0] + '\n '.join([ '\n', ]) parser = optparse.OptionParser(__doc__ and __doc__.strip()) opts, ledger, args = beancount.cmdline.main(parser) # XXX seems to parse only the first file # figure out things of interest RECONCILABLE_ACCTS_PAT = '(?i)(checking|saving|credit ?card)' reconcilable_re = re.compile(RECONCILABLE_ACCTS_PAT) def nub(xs): return list(set(xs)) # list -> list def cleared_txn(t): return t.flag=='*' # transaction -> boolean def reconcilable_posting(p): return reconcilable_re.search(p.account_name) is not None # posting -> boolean def reconcilable_txn(t): return len(filter(reconcilable_posting, t.postings)) > 0 # transaction -> boolean def reconciled_txn(t): return reconcilable_txn(t) and cleared_txn(t) # transaction -> boolean def unreconciled_txn(t): return reconcilable_txn(t) and not cleared_txn(t) # transaction -> boolean def first_of_month(d): return d.replace(day=1) # date -> date def previous_first_of_month(d): return first_of_month(first_of_month(d)-timedelta(1)) # date -> date files = ', '.join([f[0] for f in ledger.parsed_files]) txns = ledger.transactions if not txns: sys.exit('The ledger (%s) seems empty.' % files) info = ledger.dump_info() today = date.today() thirtydaysago = today - timedelta(30) monthstart = first_of_month(today) lastmonthstart = previous_first_of_month(today) pasttxns = filter(lambda t:t.actual_date <= today, txns) thirtydaytxns = filter(lambda t:t.actual_date >= thirtydaysago and t.actual_date <= today, txns) monthtxns = filter(lambda t:t.actual_date >= monthstart and t.actual_date <= today, txns) lastmonthtxns = filter(lambda t:t.actual_date >= lastmonthstart and t.actual_date < monthstart, txns) reconciledtxns = filter(reconciled_txn, txns) unreconciledtxns = filter(unreconciled_txn, pasttxns) unclearedtxns = filter(lambda t:not cleared_txn(t), pasttxns) recentdate = pasttxns[-1].actual_date reconciledate = reconciledtxns and reconciledtxns[-1].actual_date or None start = txns[0].actual_date end = txns[-1].actual_date days = max(1, (end-start).days) # print report def plural_suffix(n): return abs(n) != 1 and 's' or '' # int -> string def print_elapsed_days(date): # datetime -> string if not date: return 'never' n = (today - date).days #if n==0: return 'today' #return '%s day%s ago' % (n,plural_suffix(n)) return n def print_report(): # -> string #%(beancountinfo)s print """\ Ledger status as of %(date)s ------------------------------ File : %(files)s Period : %(start)s to %(end)s Transactions : %(txns)s (%(txnsperday)0.1f per day) Transactions last 30 days : %(thirtydaytxns)s (%(thirtydaytxnsperday)0.1f per day) Transactions this month : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s) Uncleared transactions : %(uncleared)s Days since reconciliation : %(reconcileelapsed)s Days since last transaction : %(recentelapsed)s """ % { 'date':today, 'start':start, 'end':end, 'files':files, 'beancountinfo':'\n'.join(info), 'txns':len(txns), 'days':days, 'dayssuffix':plural_suffix(days), 'txnsperday':len(txns)/float(days), 'thirtydaytxns':len(thirtydaytxns), 'thirtydaytxnsperday':len(thirtydaytxns)/30.0, 'monthtxns':len(monthtxns), 'lastmonthtxns':len(lastmonthtxns), 'unreconciled':len(unreconciledtxns), 'uncleared':len(unclearedtxns), 'reconciledate':reconciledate, 'recentdate':recentdate, 'reconcileelapsed':print_elapsed_days(reconciledate), 'recentelapsed':print_elapsed_days(recentdate), } if __name__ == '__main__': print_report()