#!/usr/bin/env python """ report [--depth=d] [day|week|month|year|| ] [[--] ledger args...] Simon Michael 2008 An easier front end for timelog reporting with ledger. Actually there's nothing timelog-specific right now, it works with ordinary ledgers too. Depth control: the --depth option sets the account display depth. Note ledger 2.5 turns on empty account display (-E) when using this. Period specification: use a period keyword or one or two dates. A single date like yyyy/mm/dd, yyyy/mm, or yyyy means that day, month or year. Ledger arguments: additional arguments will be passed through to ledger. You will need the -- if you are passing flags. Remember ledger flags must come before the ledger command. No ledger arguments means do a balance report. -s is always assumed for balance reports. Examples: timereport -> show the entire timelog's balance timereport day -> today's balance ("today" also works) timereport week -> since monday timereport month -> since the first of the month timereport 2008 --depth 1 -> in 2008, top level accounts only timereport 2008/5 -> in may timereport 2008-5-2 -> on the 2nd timereport 2008/1/15 2008/1/31 -> jan 15th to jan 31st inclusive timereport year -- -s bal work -> work hours this year timereport week -- -w register -> clock-ins this week, wide format timereport -- -A register -> average clock-in length """ import sys, os, re, optparse import calendar from datetime import datetime, date, timedelta from pprint import pprint as pp LEDGERFILE = '~/.ledger' TIMELOGFILE = '~/.timelog' FILE = (sys.argv[0].endswith('timereport') or sys.argv[0].endswith('hours')) and TIMELOGFILE or LEDGERFILE LEDGER = 'ledger2.5' # current ledger has buggy -p FORMAT = '--balance-format "%10T %2_%-a\n"' # more compact output TODAY = date.today() YEAR,MONTH,DAY = TODAY.year, TODAY.month, TODAY.day parser = optparse.OptionParser() parser.add_option('-f','--file',default=FILE,help='timelog or ledger file') parser.add_option('--depth',default=None,help='show accounts up to this depth') parser.add_option('--title',default=None,help='report title') opts,args = parser.parse_args() # utils def first_of_year(d): return d.replace(month=1,day=1) # date -> date def last_of_year(d): return d.replace(month=12,day=31) # date -> date def days_in_month(d): return calendar.monthrange(d.year,d.month)[1] # date -> integer def first_of_month(d): return d.replace(day=1) # date -> date def last_of_month(d): return d.replace(day=days_in_month(d)) # date -> date def previous_first_of_month(d): return first_of_month(first_of_month(d)-timedelta(1)) # date -> date def first_of_week(d): return d - timedelta(d.weekday()) def depth_arg(d): return d and ('-d "l<=%s"' % d) or '' # integer|None -> string def period_arg(From=None, to=None): # date|None, date|None -> string """Convert start and end dates, if any, to a ledger period argument.""" f = From and 'from %s' % From t = to and 'to %s' % (to+timedelta(1)) if f or t: return '-p "%s %s"' % (f, t) else: return '' def parse_date(s,format): # string, string -> date|None try: return datetime.strptime(s,format).date() except ValueError: return None def forgiving_parse_date(s): # string|None -> date|None return (s and (False or parse_date(s,'%Y/%m/%d') or parse_date(s,'%Y/%m') or parse_date(s,'%Y') or parse_date(s,'%Y-%m-%d') or parse_date(s,'%Y-%m') or parse_date(s,'%Y') ) or None) # argument parsing def parse_date_arg(args): # list -> (date|None, list) d = args and forgiving_parse_date(args[0]) or None return d and (d, args[1:]) or (None, args) def parse_start_end_dates(args): # list -> ((date,date)|None, list) args0 = args d1,args = parse_date_arg(args) d2,args = parse_date_arg(args) if d1 and d2: return (d1,d2),args else: return None, args0 def date_to_date_pair(s): # string -> (date, date)|None "Convert a possibly partial date string to a start, end date pair." d = parse_date(s,'%Y') if d: return first_of_year(d), last_of_year(d) d = parse_date(s,'%Y/%m') if d: return first_of_month(d), last_of_month(d) d = parse_date(s,'%Y/%m/%d') if d: return d, d else: return None def parse_single_date(args): # list -> ((date,date)|None, list) if args and forgiving_parse_date(args[0]): p = date_to_date_pair(args[0]) if p: return p, args[1:] return None, args def parse_period(args): # list -> ((date,date)|None, list) """Convert timereport period arguments to a start, end date pair (or None) and remove from the argument list. See module docs.""" dates,args = parse_start_end_dates(args) if dates: return dates,args dates,args = parse_single_date(args) if dates: return dates, args if args and args[0] == 'year' : return (first_of_year(TODAY), TODAY), args[1:] if args and args[0] == 'month': return (first_of_month(TODAY), TODAY), args[1:] if args and args[0] == 'week' : return (first_of_week(TODAY), TODAY), args[1:] if args and (args[0] == 'day' or args[0] == 'today') : return (TODAY, TODAY), args[1:] return None, args # output def underline(s): # string -> string return s + '\n' + '-'*len(s) + '\n' def command_used(): # -> string; depends on command-line args return '%s' % ' '.join([sys.argv[0].split('/')[-1]]+sys.argv[1:]) def print_time_report(): # -> None; depends on command-line args, ledger file, ledger binary; affects stdout p, ledgerargs = parse_period(args) periodarg = p and period_arg(*p) or '' if not ledgerargs: ledgerargs = ['balance'] for i in range(len(ledgerargs)): if ledgerargs[i].startswith('bal'): ledgerargs[i] = '-s '+ledgerargs[i] break cmd = ' '.join([LEDGER, '-f', opts.file, FORMAT, depth_arg(opts.depth), periodarg]+ledgerargs) if opts.title != None: title = opts.title else: title = None # else: # title = "Ledger report" + (p and " for %s to %s" % (p[0],p[1]) or '') # title += ' (%s)' % command_used() if title: print underline(title), print os.popen(cmd).read() #print 'using: %s' % `cmd` if __name__ == '__main__': print_time_report()